├── .gitignore ├── 2020년_11월_일별_주문_금액.csv ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── output └── 2020년_11월_일별_주문_금액.csv ├── settings.gradle └── src ├── main ├── java │ └── fastcampus │ │ └── spring │ │ └── batch │ │ ├── SpringBatchExampleApplication.java │ │ ├── part1 │ │ └── HelloConfiguration.java │ │ ├── part2 │ │ └── SharedConfiguration.java │ │ ├── part3 │ │ ├── ChunkProcessingConfiguration.java │ │ ├── CustomItemReader.java │ │ ├── DuplicateValidationProcessor.java │ │ ├── ItemProcessorConfiguration.java │ │ ├── ItemReaderConfiguration.java │ │ ├── ItemWriterConfiguration.java │ │ ├── NotFoundNameException.java │ │ ├── Person.java │ │ ├── PersonRepository.java │ │ ├── PersonValidationRetryProcessor.java │ │ ├── SavePersonConfiguration.java │ │ └── SavePersonListener.java │ │ ├── part4 │ │ ├── LevelUpJobExecutionListener.java │ │ ├── SaveUserTasklet.java │ │ ├── User.java │ │ ├── UserConfiguration.java │ │ └── UserRepository.java │ │ ├── part5 │ │ ├── JobParametersDecide.java │ │ ├── OrderStatistics.java │ │ └── Orders.java │ │ └── part6 │ │ ├── AsyncUserConfiguration.java │ │ ├── MultiThreadUserConfiguration.java │ │ ├── ParallelUserConfiguration.java │ │ ├── PartitionUserConfiguration.java │ │ └── UserLevelUpPartitioner.java └── resources │ ├── application-mysql.yml │ ├── application.yml │ ├── person.csv │ ├── person.sql │ └── test.csv └── test └── java └── fastcampus └── spring └── batch ├── SpringBatchExampleApplicationTests.java ├── TestConfiguration.java ├── part3 └── SavePersonConfigurationTest.java └── part4 └── ParallelUserConfigurationTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /2020년_11월_일별_주문_금액.csv: -------------------------------------------------------------------------------- 1 | total_amoun,date 2 | 10000000,2020-11-01 3 | 2000000000,2020-11-02 4 | 3000000000,2020-11-03 5 | 5000000000,2020-11-04 6 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.4.2' 3 | id 'io.spring.dependency-management' version '1.0.11.RELEASE' 4 | id 'java' 5 | } 6 | 7 | group = 'fastcampus.spring.batch' 8 | version = '0.0.1-SNAPSHOT' 9 | sourceCompatibility = '1.8' 10 | 11 | configurations { 12 | compileOnly { 13 | extendsFrom annotationProcessor 14 | } 15 | } 16 | 17 | repositories { 18 | mavenCentral() 19 | } 20 | 21 | dependencies { 22 | implementation 'org.springframework.boot:spring-boot-starter-batch' 23 | implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' 24 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 25 | implementation 'org.springframework.batch:spring-batch-integration' 26 | 27 | compileOnly 'org.projectlombok:lombok' 28 | runtimeOnly 'com.h2database:h2' 29 | runtimeOnly 'mysql:mysql-connector-java' 30 | annotationProcessor 'org.projectlombok:lombok' 31 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 32 | testImplementation 'org.springframework.batch:spring-batch-test' 33 | } 34 | 35 | test { 36 | useJUnitPlatform() 37 | } 38 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woniper/fastcampus-spring-batch-example/01ce788670564e9830300fb1e529bf4770f7f7c5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /output/2020년_11월_일별_주문_금액.csv: -------------------------------------------------------------------------------- 1 | total_amoun,date 2 | 10000000,2020-11-01 3 | 2000000000,2020-11-02 4 | 3000000000,2020-11-03 5 | 5000000000,2020-11-04 6 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'spring-batch-example' 2 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/SpringBatchExampleApplication.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch; 2 | 3 | import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Primary; 8 | import org.springframework.core.task.TaskExecutor; 9 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 10 | 11 | @SpringBootApplication 12 | @EnableBatchProcessing 13 | public class SpringBatchExampleApplication { 14 | 15 | public static void main(String[] args) { 16 | System.exit(SpringApplication.exit(SpringApplication.run(SpringBatchExampleApplication.class, args))); 17 | } 18 | 19 | @Bean 20 | @Primary 21 | TaskExecutor taskExecutor() { 22 | ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); 23 | taskExecutor.setCorePoolSize(10); 24 | taskExecutor.setMaxPoolSize(20); 25 | taskExecutor.setThreadNamePrefix("batch-thread-"); 26 | taskExecutor.initialize(); 27 | return taskExecutor; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part1/HelloConfiguration.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part1; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.batch.core.Job; 5 | import org.springframework.batch.core.Step; 6 | import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; 7 | import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; 8 | import org.springframework.batch.core.launch.support.RunIdIncrementer; 9 | import org.springframework.batch.repeat.RepeatStatus; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | @Configuration 14 | @Slf4j 15 | public class HelloConfiguration { 16 | 17 | private final JobBuilderFactory jobBuilderFactory; 18 | private final StepBuilderFactory stepBuilderFactory; 19 | 20 | public HelloConfiguration(JobBuilderFactory jobBuilderFactory, 21 | StepBuilderFactory stepBuilderFactory) { 22 | this.jobBuilderFactory = jobBuilderFactory; 23 | this.stepBuilderFactory = stepBuilderFactory; 24 | } 25 | 26 | @Bean 27 | public Job helloJob() { 28 | return jobBuilderFactory.get("helloJob") 29 | .incrementer(new RunIdIncrementer()) 30 | .start(this.helloStep()) 31 | .build(); 32 | } 33 | 34 | @Bean 35 | public Step helloStep() { 36 | return stepBuilderFactory.get("helloStep") 37 | .tasklet((contribution, chunkContext) -> { 38 | log.info("hello spring batch"); 39 | return RepeatStatus.FINISHED; 40 | }).build(); 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part2/SharedConfiguration.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part2; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.batch.core.*; 5 | import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; 6 | import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; 7 | import org.springframework.batch.core.launch.support.RunIdIncrementer; 8 | import org.springframework.batch.item.ExecutionContext; 9 | import org.springframework.batch.repeat.RepeatStatus; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | @Configuration 14 | @Slf4j 15 | public class SharedConfiguration { 16 | 17 | private final JobBuilderFactory jobBuilderFactory; 18 | private final StepBuilderFactory stepBuilderFactory; 19 | 20 | public SharedConfiguration(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory) { 21 | this.jobBuilderFactory = jobBuilderFactory; 22 | this.stepBuilderFactory = stepBuilderFactory; 23 | } 24 | 25 | @Bean 26 | public Job shareJob() { 27 | return jobBuilderFactory.get("shareJob") 28 | .incrementer(new RunIdIncrementer()) 29 | .start(this.shareStep1()) 30 | .next(this.shareStep2()) 31 | .build(); 32 | } 33 | 34 | @Bean 35 | public Step shareStep2() { 36 | return stepBuilderFactory.get("shareStep2") 37 | .tasklet((contribution, chunkContext) -> { 38 | // step ExecutionContext.get 39 | StepExecution stepExecution = contribution.getStepExecution(); 40 | ExecutionContext stepExecutionContext = stepExecution.getExecutionContext(); 41 | 42 | // job ExecutionContext.get 43 | JobExecution jobExecution = stepExecution.getJobExecution(); 44 | ExecutionContext jobExecutionContext = jobExecution.getExecutionContext(); 45 | 46 | // log 47 | log.info("jobValue : {}, stepValue : {}", 48 | jobExecutionContext.getString("job", "emptyJob"), 49 | stepExecutionContext.getString("step", "emptyStep")); 50 | 51 | return RepeatStatus.FINISHED; 52 | 53 | }).build(); 54 | } 55 | 56 | @Bean 57 | public Step shareStep1() { 58 | return stepBuilderFactory.get("shareStep1") 59 | .tasklet((contribution, chunkContext) -> { 60 | // step ExecutionContext.put 61 | StepExecution stepExecution = contribution.getStepExecution(); 62 | ExecutionContext stepExecutionContext = stepExecution.getExecutionContext(); 63 | stepExecutionContext.putString("step", "step execution context"); 64 | 65 | // job ExecutionContext.put 66 | JobExecution jobExecution = stepExecution.getJobExecution(); 67 | ExecutionContext jobExecutionContext = jobExecution.getExecutionContext(); 68 | jobExecutionContext.putString("job", "job execution context"); 69 | 70 | // log 71 | JobInstance jobInstance = jobExecution.getJobInstance(); 72 | JobParameters jobParameters = jobExecution.getJobParameters(); 73 | // JobParameters jobParameters1 = stepExecution.getJobParameters(); 74 | 75 | log.info("jobName : {}, stepName : {}, run.id : {}", 76 | jobInstance.getJobName(), 77 | stepExecution.getStepName(), 78 | jobParameters.getLong("run.id")); 79 | 80 | return RepeatStatus.FINISHED; 81 | }).build(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part3/ChunkProcessingConfiguration.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part3; 2 | 3 | import io.micrometer.core.instrument.util.StringUtils; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.batch.core.Job; 6 | import org.springframework.batch.core.Step; 7 | import org.springframework.batch.core.StepExecution; 8 | import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; 9 | import org.springframework.batch.core.configuration.annotation.JobScope; 10 | import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; 11 | import org.springframework.batch.core.configuration.annotation.StepScope; 12 | import org.springframework.batch.core.launch.support.RunIdIncrementer; 13 | import org.springframework.batch.core.step.tasklet.Tasklet; 14 | import org.springframework.batch.item.ItemProcessor; 15 | import org.springframework.batch.item.ItemReader; 16 | import org.springframework.batch.item.ItemWriter; 17 | import org.springframework.batch.item.support.ListItemReader; 18 | import org.springframework.batch.repeat.RepeatStatus; 19 | import org.springframework.beans.factory.annotation.Value; 20 | import org.springframework.context.annotation.Bean; 21 | import org.springframework.context.annotation.Configuration; 22 | 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | 26 | @Configuration 27 | @Slf4j 28 | public class ChunkProcessingConfiguration { 29 | 30 | private final JobBuilderFactory jobBuilderFactory; 31 | private final StepBuilderFactory stepBuilderFactory; 32 | 33 | public ChunkProcessingConfiguration(JobBuilderFactory jobBuilderFactory, 34 | StepBuilderFactory stepBuilderFactory) { 35 | this.jobBuilderFactory = jobBuilderFactory; 36 | this.stepBuilderFactory = stepBuilderFactory; 37 | } 38 | 39 | @Bean 40 | public Job chunkProcessingJob() { 41 | return jobBuilderFactory.get("chunkProcessingJob") 42 | .incrementer(new RunIdIncrementer()) 43 | .start(this.taskBaseStep()) 44 | .next(this.chunkBaseStep(null)) 45 | .build(); 46 | } 47 | 48 | @Bean 49 | @JobScope 50 | public Step chunkBaseStep(@Value("#{jobParameters[chunkSize]}") String chunkSize) { 51 | 52 | return stepBuilderFactory.get("chunkBaseStep") 53 | .chunk(StringUtils.isNotEmpty(chunkSize) ? Integer.parseInt(chunkSize) : 10) 54 | .reader(itemReader()) 55 | .processor(itemProcessor()) 56 | .writer(itemWriter()) 57 | .build(); 58 | } 59 | 60 | private ItemWriter itemWriter() { 61 | return items -> log.info("chunk item size : {}", items.size()); 62 | // return items -> items.forEach(log::info); 63 | } 64 | 65 | private ItemProcessor itemProcessor() { 66 | return item -> item + ", Spring Batch"; 67 | } 68 | 69 | private ItemReader itemReader() { 70 | return new ListItemReader<>(getItems()); 71 | } 72 | 73 | @Bean 74 | public Step taskBaseStep() { 75 | return stepBuilderFactory.get("taskBaseStep") 76 | .tasklet(this.tasklet(null)) 77 | .build(); 78 | } 79 | 80 | @Bean 81 | @StepScope 82 | public Tasklet tasklet(@Value("#{jobParameters[chunkSize]}") String value) { 83 | List items = getItems(); 84 | 85 | return (contribution, chunkContext) -> { 86 | StepExecution stepExecution = contribution.getStepExecution(); 87 | // JobParameters jobParameters = stepExecution.getJobParameters(); 88 | // String value = jobParameters.getString("chunkSize", "10"); 89 | 90 | int chunkSize = StringUtils.isNotEmpty(value) ? Integer.parseInt(value) : 10; 91 | 92 | int fromIndex = stepExecution.getReadCount(); 93 | int toIndex = fromIndex + chunkSize; 94 | 95 | if (fromIndex >= items.size()) { 96 | return RepeatStatus.FINISHED; 97 | } 98 | 99 | List subList = items.subList(fromIndex, toIndex); 100 | 101 | log.info("task item size : {}", subList.size()); 102 | 103 | stepExecution.setReadCount(toIndex); 104 | 105 | return RepeatStatus.CONTINUABLE; 106 | }; 107 | } 108 | 109 | private List getItems() { 110 | List items = new ArrayList<>(); 111 | 112 | for (int i = 0; i < 100; i++) { 113 | items.add(i + " Hello"); 114 | } 115 | 116 | return items; 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part3/CustomItemReader.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part3; 2 | 3 | import org.springframework.batch.item.ItemReader; 4 | import org.springframework.batch.item.NonTransientResourceException; 5 | import org.springframework.batch.item.ParseException; 6 | import org.springframework.batch.item.UnexpectedInputException; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | public class CustomItemReader implements ItemReader { 12 | 13 | private final List items; 14 | 15 | public CustomItemReader(List items) { 16 | this.items = new ArrayList<>(items); 17 | } 18 | 19 | @Override 20 | public T read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException { 21 | if (!items.isEmpty()) { 22 | return items.remove(0); 23 | } 24 | 25 | return null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part3/DuplicateValidationProcessor.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part3; 2 | 3 | import org.springframework.batch.item.ItemProcessor; 4 | 5 | import java.util.Map; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | import java.util.function.Function; 8 | 9 | public class DuplicateValidationProcessor implements ItemProcessor { 10 | 11 | private final Map keyPool = new ConcurrentHashMap<>(); 12 | private final Function keyExtractor; 13 | private final boolean allowDuplicate; 14 | 15 | public DuplicateValidationProcessor(Function keyExtractor, 16 | boolean allowDuplicate) { 17 | 18 | this.keyExtractor = keyExtractor; 19 | this.allowDuplicate = allowDuplicate; 20 | } 21 | 22 | @Override 23 | public T process(T item) throws Exception { 24 | if (allowDuplicate) { 25 | return item; 26 | } 27 | 28 | String key = keyExtractor.apply(item); 29 | 30 | if (keyPool.containsKey(key)) { 31 | return null; 32 | } 33 | 34 | keyPool.put(key, key); 35 | return item; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part3/ItemProcessorConfiguration.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part3; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.batch.core.Job; 5 | import org.springframework.batch.core.Step; 6 | import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; 7 | import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; 8 | import org.springframework.batch.core.launch.support.RunIdIncrementer; 9 | import org.springframework.batch.item.ItemProcessor; 10 | import org.springframework.batch.item.ItemReader; 11 | import org.springframework.batch.item.ItemWriter; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | @Configuration 19 | @Slf4j 20 | public class ItemProcessorConfiguration { 21 | 22 | private final JobBuilderFactory jobBuilderFactory; 23 | private final StepBuilderFactory stepBuilderFactory; 24 | 25 | public ItemProcessorConfiguration(JobBuilderFactory jobBuilderFactory, 26 | StepBuilderFactory stepBuilderFactory) { 27 | this.jobBuilderFactory = jobBuilderFactory; 28 | this.stepBuilderFactory = stepBuilderFactory; 29 | } 30 | 31 | @Bean 32 | public Job itemProcessorJob() { 33 | return this.jobBuilderFactory.get("itemProcessorJob") 34 | .incrementer(new RunIdIncrementer()) 35 | .start(this.itemProcessorStep()) 36 | .build(); 37 | } 38 | 39 | @Bean 40 | public Step itemProcessorStep() { 41 | return this.stepBuilderFactory.get("itemProcessorStep") 42 | .chunk(10) 43 | .reader(itemReader()) 44 | .processor(itemProcessor()) 45 | .writer(itemWriter()) 46 | .build(); 47 | } 48 | 49 | private ItemWriter itemWriter() { 50 | return items -> items.forEach(x -> log.info("PERSON.ID : {}", x.getId())); 51 | } 52 | 53 | private ItemProcessor itemProcessor() { 54 | return item -> { 55 | if (item.getId() % 2 == 0) { 56 | return item; 57 | } 58 | 59 | return null; 60 | }; 61 | } 62 | 63 | private ItemReader itemReader() { 64 | return new CustomItemReader<>(getItems()); 65 | } 66 | 67 | private List getItems() { 68 | List items = new ArrayList<>(); 69 | 70 | for (int i = 0; i < 10; i++) { 71 | items.add(new Person(i + 1, "test name" + i, "test age", "test address")); 72 | } 73 | 74 | return items; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part3/ItemReaderConfiguration.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part3; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.batch.core.Job; 5 | import org.springframework.batch.core.Step; 6 | import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; 7 | import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; 8 | import org.springframework.batch.core.launch.support.RunIdIncrementer; 9 | import org.springframework.batch.item.ItemWriter; 10 | import org.springframework.batch.item.database.JdbcCursorItemReader; 11 | import org.springframework.batch.item.database.JpaCursorItemReader; 12 | import org.springframework.batch.item.database.builder.JdbcCursorItemReaderBuilder; 13 | import org.springframework.batch.item.database.builder.JpaCursorItemReaderBuilder; 14 | import org.springframework.batch.item.file.FlatFileItemReader; 15 | import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder; 16 | import org.springframework.batch.item.file.mapping.DefaultLineMapper; 17 | import org.springframework.batch.item.file.transform.DelimitedLineTokenizer; 18 | import org.springframework.context.annotation.Bean; 19 | import org.springframework.context.annotation.Configuration; 20 | import org.springframework.core.io.ClassPathResource; 21 | 22 | import javax.persistence.EntityManagerFactory; 23 | import javax.sql.DataSource; 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | import java.util.stream.Collectors; 27 | 28 | @Configuration 29 | @Slf4j 30 | public class ItemReaderConfiguration { 31 | 32 | private final JobBuilderFactory jobBuilderFactory; 33 | private final StepBuilderFactory stepBuilderFactory; 34 | private final DataSource dataSource; 35 | private final EntityManagerFactory entityManagerFactory; 36 | 37 | public ItemReaderConfiguration(JobBuilderFactory jobBuilderFactory, 38 | StepBuilderFactory stepBuilderFactory, 39 | DataSource dataSource, 40 | EntityManagerFactory entityManagerFactory) { 41 | 42 | this.jobBuilderFactory = jobBuilderFactory; 43 | this.stepBuilderFactory = stepBuilderFactory; 44 | this.dataSource = dataSource; 45 | this.entityManagerFactory = entityManagerFactory; 46 | } 47 | 48 | @Bean 49 | public Job itemReaderJob() throws Exception { 50 | return this.jobBuilderFactory.get("itemReaderJob") 51 | .incrementer(new RunIdIncrementer()) 52 | .start(this.customItemReaderStep()) 53 | .next(this.csvFileStep()) 54 | .next(this.jdbcStep()) 55 | .next(this.jpaStep()) 56 | .build(); 57 | } 58 | 59 | @Bean 60 | public Step customItemReaderStep() { 61 | return this.stepBuilderFactory.get("customItemReaderStep") 62 | .chunk(10) 63 | .reader(new CustomItemReader<>(getItems())) 64 | .writer(itemWriter()) 65 | .build(); 66 | } 67 | 68 | @Bean 69 | public Step csvFileStep() throws Exception { 70 | return stepBuilderFactory.get("csvFileStep") 71 | .chunk(10) 72 | .reader(this.csvFileItemReader()) 73 | .writer(itemWriter()) 74 | .build(); 75 | } 76 | 77 | @Bean 78 | public Step jdbcStep() throws Exception { 79 | return stepBuilderFactory.get("jdbcStep") 80 | .chunk(10) 81 | .reader(jdbcCursorItemReader()) 82 | .writer(itemWriter()) 83 | .build(); 84 | } 85 | 86 | @Bean 87 | public Step jpaStep() throws Exception { 88 | return stepBuilderFactory.get("jpaStep") 89 | .chunk(10) 90 | .reader(this.jpaCursorItemReader()) 91 | .writer(itemWriter()) 92 | .build(); 93 | } 94 | 95 | private JpaCursorItemReader jpaCursorItemReader() throws Exception { 96 | JpaCursorItemReader itemReader = new JpaCursorItemReaderBuilder() 97 | .name("jpaCursorItemReader") 98 | .entityManagerFactory(entityManagerFactory) 99 | .queryString("select p from Person p") 100 | .build(); 101 | itemReader.afterPropertiesSet(); 102 | 103 | return itemReader; 104 | } 105 | 106 | private JdbcCursorItemReader jdbcCursorItemReader() throws Exception { 107 | JdbcCursorItemReader itemReader = new JdbcCursorItemReaderBuilder() 108 | .name("jdbcCursorItemReader") 109 | .dataSource(dataSource) 110 | .sql("select id, name, age, address from person") 111 | .rowMapper((rs, rowNum) -> new Person( 112 | rs.getInt(1), rs.getString(2), rs.getString(3), rs.getString(4))) 113 | .build(); 114 | itemReader.afterPropertiesSet(); 115 | return itemReader; 116 | } 117 | 118 | private FlatFileItemReader csvFileItemReader() throws Exception { 119 | DefaultLineMapper lineMapper = new DefaultLineMapper<>(); 120 | DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer(); 121 | tokenizer.setNames("id", "name", "age", "address"); 122 | lineMapper.setLineTokenizer(tokenizer); 123 | 124 | lineMapper.setFieldSetMapper(fieldSet -> { 125 | int id = fieldSet.readInt("id"); 126 | String name = fieldSet.readString("name"); 127 | String age = fieldSet.readString("age"); 128 | String address = fieldSet.readString("address"); 129 | 130 | return new Person(id, name, age, address); 131 | }); 132 | 133 | FlatFileItemReader itemReader = new FlatFileItemReaderBuilder() 134 | .name("csvFileItemReader") 135 | .encoding("UTF-8") 136 | .resource(new ClassPathResource("test.csv")) 137 | .linesToSkip(1) 138 | .lineMapper(lineMapper) 139 | .build(); 140 | itemReader.afterPropertiesSet(); 141 | 142 | return itemReader; 143 | } 144 | 145 | private ItemWriter itemWriter() { 146 | return items -> log.info(items.stream() 147 | .map(Person::getName) 148 | .collect(Collectors.joining(", "))); 149 | } 150 | 151 | private List getItems() { 152 | List items = new ArrayList<>(); 153 | 154 | for (int i = 0; i < 10; i++) { 155 | items.add(new Person(i + 1, "test name" + i, "test age", "test address")); 156 | } 157 | 158 | return items; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part3/ItemWriterConfiguration.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part3; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.batch.core.Job; 5 | import org.springframework.batch.core.Step; 6 | import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; 7 | import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; 8 | import org.springframework.batch.core.launch.support.RunIdIncrementer; 9 | import org.springframework.batch.item.ItemReader; 10 | import org.springframework.batch.item.ItemWriter; 11 | import org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider; 12 | import org.springframework.batch.item.database.JdbcBatchItemWriter; 13 | import org.springframework.batch.item.database.JpaItemWriter; 14 | import org.springframework.batch.item.database.builder.JdbcBatchItemWriterBuilder; 15 | import org.springframework.batch.item.database.builder.JpaItemWriterBuilder; 16 | import org.springframework.batch.item.file.FlatFileItemWriter; 17 | import org.springframework.batch.item.file.builder.FlatFileItemWriterBuilder; 18 | import org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor; 19 | import org.springframework.batch.item.file.transform.DelimitedLineAggregator; 20 | import org.springframework.context.annotation.Bean; 21 | import org.springframework.context.annotation.Configuration; 22 | import org.springframework.core.io.FileSystemResource; 23 | 24 | import javax.persistence.EntityManagerFactory; 25 | import javax.sql.DataSource; 26 | import java.util.ArrayList; 27 | import java.util.List; 28 | 29 | @Configuration 30 | @Slf4j 31 | public class ItemWriterConfiguration { 32 | 33 | private final JobBuilderFactory jobBuilderFactory; 34 | private final StepBuilderFactory stepBuilderFactory; 35 | private final DataSource dataSource; 36 | private final EntityManagerFactory entityManagerFactory; 37 | 38 | public ItemWriterConfiguration(JobBuilderFactory jobBuilderFactory, 39 | StepBuilderFactory stepBuilderFactory, 40 | DataSource dataSource, 41 | EntityManagerFactory entityManagerFactory) { 42 | 43 | this.jobBuilderFactory = jobBuilderFactory; 44 | this.stepBuilderFactory = stepBuilderFactory; 45 | this.dataSource = dataSource; 46 | this.entityManagerFactory = entityManagerFactory; 47 | } 48 | 49 | @Bean 50 | public Job itemWriterJob() throws Exception { 51 | return this.jobBuilderFactory.get("itemWriterJob") 52 | .incrementer(new RunIdIncrementer()) 53 | .start(this.csvItemWriterStep()) 54 | // .next(this.jdbcBatchItemWriterStep()) 55 | .next(this.jpaItemWriterStep()) 56 | .build(); 57 | } 58 | 59 | @Bean 60 | public Step csvItemWriterStep() throws Exception { 61 | return this.stepBuilderFactory.get("csvItemWriterStep") 62 | .chunk(10) 63 | .reader(itemReader()) 64 | .writer(csvFileItemWriter()) 65 | .build(); 66 | } 67 | 68 | @Bean 69 | public Step jdbcBatchItemWriterStep() { 70 | return stepBuilderFactory.get("jdbcBatchItemWriterStep") 71 | .chunk(10) 72 | .reader(itemReader()) 73 | .writer(jdbcBatchItemWriter()) 74 | .build(); 75 | } 76 | 77 | @Bean 78 | public Step jpaItemWriterStep() throws Exception { 79 | return stepBuilderFactory.get("jpaItemWriterStep") 80 | .chunk(10) 81 | .reader(itemReader()) 82 | .writer(jpaItemWriter()) 83 | .build(); 84 | } 85 | 86 | private ItemWriter jpaItemWriter() throws Exception { 87 | JpaItemWriter itemWriter = new JpaItemWriterBuilder() 88 | .entityManagerFactory(entityManagerFactory) 89 | // .usePersist(true) 90 | .build(); 91 | 92 | itemWriter.afterPropertiesSet(); 93 | return itemWriter; 94 | } 95 | 96 | private ItemWriter jdbcBatchItemWriter() { 97 | JdbcBatchItemWriter itemWriter = new JdbcBatchItemWriterBuilder() 98 | .dataSource(dataSource) 99 | .itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>()) 100 | .sql("insert into person(name, age, address) values(:name, :age, :address)") 101 | .build(); 102 | 103 | itemWriter.afterPropertiesSet(); 104 | 105 | return itemWriter; 106 | } 107 | 108 | private ItemWriter csvFileItemWriter() throws Exception { 109 | BeanWrapperFieldExtractor fieldExtractor = new BeanWrapperFieldExtractor<>(); 110 | fieldExtractor.setNames(new String[] {"id", "name", "age", "address"}); 111 | 112 | DelimitedLineAggregator lineAggregator = new DelimitedLineAggregator<>(); 113 | lineAggregator.setDelimiter(","); 114 | lineAggregator.setFieldExtractor(fieldExtractor); 115 | 116 | FlatFileItemWriter itemWriter = new FlatFileItemWriterBuilder() 117 | .name("csvFileItemWriter") 118 | .encoding("UTF-8") 119 | .resource(new FileSystemResource("output/test-output.csv")) 120 | .lineAggregator(lineAggregator) 121 | .headerCallback(writer -> writer.write("id,이름,나이,거주지")) 122 | .footerCallback(writer -> writer.write("-------------------\n")) 123 | .append(true) 124 | .build(); 125 | 126 | itemWriter.afterPropertiesSet(); 127 | 128 | return itemWriter; 129 | } 130 | 131 | private ItemReader itemReader() { 132 | return new CustomItemReader<>(getItems()); 133 | } 134 | 135 | private List getItems() { 136 | List items = new ArrayList<>(); 137 | 138 | for (int i = 0; i < 100; i++) { 139 | items.add(new Person("test name" + i, "test age", "test address")); 140 | } 141 | 142 | return items; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part3/NotFoundNameException.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part3; 2 | 3 | public class NotFoundNameException extends RuntimeException { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part3/Person.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part3; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | 6 | import javax.persistence.Entity; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.GenerationType; 9 | import javax.persistence.Id; 10 | import java.util.Objects; 11 | 12 | @Entity 13 | @NoArgsConstructor 14 | @Getter 15 | public class Person { 16 | 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | private int id; 20 | private String name; 21 | private String age; 22 | private String address; 23 | 24 | public Person(String name, String age, String address) { 25 | this(0, name, age, address); 26 | } 27 | 28 | public Person(int id, String name, String age, String address) { 29 | this.id = id; 30 | this.name = name; 31 | this.age = age; 32 | this.address = address; 33 | } 34 | 35 | public boolean isNotEmptyName() { 36 | return Objects.nonNull(this.name) && !name.isEmpty(); 37 | } 38 | 39 | public Person unknownName() { 40 | this.name = "UNKNOWN"; 41 | return this; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part3/PersonRepository.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part3; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | public interface PersonRepository extends JpaRepository { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part3/PersonValidationRetryProcessor.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part3; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.batch.item.ItemProcessor; 5 | import org.springframework.retry.RetryCallback; 6 | import org.springframework.retry.RetryContext; 7 | import org.springframework.retry.RetryListener; 8 | import org.springframework.retry.support.RetryTemplate; 9 | import org.springframework.retry.support.RetryTemplateBuilder; 10 | 11 | @Slf4j 12 | public class PersonValidationRetryProcessor implements ItemProcessor { 13 | 14 | private final RetryTemplate retryTemplate; 15 | 16 | public PersonValidationRetryProcessor() { 17 | this.retryTemplate = new RetryTemplateBuilder() 18 | .maxAttempts(3) 19 | .retryOn(NotFoundNameException.class) 20 | .withListener(new SavePersonRetryListener()) 21 | .build(); 22 | 23 | } 24 | 25 | @Override 26 | public Person process(Person item) throws Exception { 27 | return this.retryTemplate.execute(context -> { 28 | // RetryCallback 29 | 30 | if (item.isNotEmptyName()) { 31 | return item; 32 | } 33 | 34 | throw new NotFoundNameException(); 35 | }, context -> { 36 | // RecoveryCallback 37 | return item.unknownName(); 38 | }); 39 | } 40 | 41 | public static class SavePersonRetryListener implements RetryListener { 42 | @Override 43 | public boolean open(RetryContext context, RetryCallback callback) { 44 | return true; 45 | } 46 | 47 | @Override 48 | public void close(RetryContext context, RetryCallback callback, Throwable throwable) { 49 | log.info("close"); 50 | } 51 | 52 | @Override 53 | public void onError(RetryContext context, RetryCallback callback, Throwable throwable) { 54 | log.info("onError"); 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part3/SavePersonConfiguration.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part3; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.batch.core.Job; 5 | import org.springframework.batch.core.Step; 6 | import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; 7 | import org.springframework.batch.core.configuration.annotation.JobScope; 8 | import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; 9 | import org.springframework.batch.core.launch.support.RunIdIncrementer; 10 | import org.springframework.batch.item.ItemProcessor; 11 | import org.springframework.batch.item.ItemReader; 12 | import org.springframework.batch.item.ItemWriter; 13 | import org.springframework.batch.item.database.JpaItemWriter; 14 | import org.springframework.batch.item.database.builder.JpaItemWriterBuilder; 15 | import org.springframework.batch.item.file.FlatFileItemReader; 16 | import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder; 17 | import org.springframework.batch.item.file.mapping.DefaultLineMapper; 18 | import org.springframework.batch.item.file.transform.DelimitedLineTokenizer; 19 | import org.springframework.batch.item.support.CompositeItemProcessor; 20 | import org.springframework.batch.item.support.CompositeItemWriter; 21 | import org.springframework.batch.item.support.builder.CompositeItemProcessorBuilder; 22 | import org.springframework.batch.item.support.builder.CompositeItemWriterBuilder; 23 | import org.springframework.beans.factory.annotation.Value; 24 | import org.springframework.context.annotation.Bean; 25 | import org.springframework.context.annotation.Configuration; 26 | import org.springframework.core.io.ClassPathResource; 27 | 28 | import javax.persistence.EntityManagerFactory; 29 | 30 | @Configuration 31 | @Slf4j 32 | public class SavePersonConfiguration { 33 | 34 | private final JobBuilderFactory jobBuilderFactory; 35 | private final StepBuilderFactory stepBuilderFactory; 36 | private final EntityManagerFactory entityManagerFactory; 37 | 38 | public SavePersonConfiguration(JobBuilderFactory jobBuilderFactory, 39 | StepBuilderFactory stepBuilderFactory, 40 | EntityManagerFactory entityManagerFactory) { 41 | this.jobBuilderFactory = jobBuilderFactory; 42 | this.stepBuilderFactory = stepBuilderFactory; 43 | this.entityManagerFactory = entityManagerFactory; 44 | } 45 | 46 | @Bean 47 | public Job savePersonJob() throws Exception { 48 | return this.jobBuilderFactory.get("savePersonJob") 49 | .incrementer(new RunIdIncrementer()) 50 | .start(this.savePersonStep(null)) 51 | .listener(new SavePersonListener.SavePersonJobExecutionListener()) 52 | .listener(new SavePersonListener.SavePersonAnnotationJobExecutionListener()) 53 | .build(); 54 | } 55 | 56 | @Bean 57 | @JobScope 58 | public Step savePersonStep(@Value("#{jobParameters[allow_duplicate]}") String allowDuplicate) throws Exception { 59 | return this.stepBuilderFactory.get("savePersonStep") 60 | .chunk(10) 61 | .reader(itemReader()) 62 | .processor(itemProcessor(allowDuplicate)) 63 | .writer(itemWriter()) 64 | .listener(new SavePersonListener.SavePersonStepExecutionListener()) 65 | .faultTolerant() 66 | .skip(NotFoundNameException.class) 67 | .skipLimit(2) 68 | .build(); 69 | } 70 | 71 | private ItemProcessor itemProcessor(String allowDuplicate) throws Exception { 72 | DuplicateValidationProcessor duplicateValidationProcessor = 73 | new DuplicateValidationProcessor<>(Person::getName, Boolean.parseBoolean(allowDuplicate)); 74 | 75 | ItemProcessor validationProcessor = item -> { 76 | if (item.isNotEmptyName()) { 77 | return item; 78 | } 79 | 80 | throw new NotFoundNameException(); 81 | }; 82 | 83 | CompositeItemProcessor itemProcessor = new CompositeItemProcessorBuilder() 84 | .delegates(new PersonValidationRetryProcessor(), validationProcessor, duplicateValidationProcessor) 85 | .build(); 86 | 87 | itemProcessor.afterPropertiesSet(); 88 | 89 | return itemProcessor; 90 | } 91 | 92 | private ItemWriter itemWriter() throws Exception { 93 | // return items -> items.forEach(x -> log.info("저는 {} 입니다.", x.getName())); 94 | JpaItemWriter jpaItemWriter = new JpaItemWriterBuilder() 95 | .entityManagerFactory(entityManagerFactory) 96 | .build(); 97 | 98 | ItemWriter logItemWriter = items -> log.info("person.size : {}", items.size()); 99 | 100 | CompositeItemWriter itemWriter = new CompositeItemWriterBuilder() 101 | .delegates(jpaItemWriter, logItemWriter) 102 | .build(); 103 | 104 | itemWriter.afterPropertiesSet(); 105 | return itemWriter; 106 | } 107 | 108 | private ItemReader itemReader() throws Exception { 109 | DefaultLineMapper lineMapper = new DefaultLineMapper<>(); 110 | DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer(); 111 | lineTokenizer.setNames("name", "age", "address"); 112 | lineMapper.setLineTokenizer(lineTokenizer); 113 | lineMapper.setFieldSetMapper(fieldSet -> new Person( 114 | fieldSet.readString(0), 115 | fieldSet.readString(1), 116 | fieldSet.readString(2))); 117 | 118 | FlatFileItemReader itemReader = new FlatFileItemReaderBuilder() 119 | .name("savePersonItemReader") 120 | .encoding("UTF-8") 121 | .linesToSkip(1) 122 | .resource(new ClassPathResource("person.csv")) 123 | .lineMapper(lineMapper) 124 | .build(); 125 | 126 | itemReader.afterPropertiesSet(); 127 | return itemReader; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part3/SavePersonListener.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part3; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.batch.core.ExitStatus; 5 | import org.springframework.batch.core.JobExecution; 6 | import org.springframework.batch.core.JobExecutionListener; 7 | import org.springframework.batch.core.StepExecution; 8 | import org.springframework.batch.core.annotation.AfterJob; 9 | import org.springframework.batch.core.annotation.AfterStep; 10 | import org.springframework.batch.core.annotation.BeforeJob; 11 | import org.springframework.batch.core.annotation.BeforeStep; 12 | 13 | @Slf4j 14 | public class SavePersonListener { 15 | 16 | public static class SavePersonStepExecutionListener { 17 | @BeforeStep 18 | public void beforeStep(StepExecution stepExecution) { 19 | log.info("beforeStep"); 20 | } 21 | 22 | @AfterStep 23 | public ExitStatus afterStep(StepExecution stepExecution) { 24 | log.info("afterStep : {}", stepExecution.getWriteCount()); 25 | 26 | return stepExecution.getExitStatus(); 27 | } 28 | } 29 | 30 | public static class SavePersonJobExecutionListener implements JobExecutionListener { 31 | 32 | @Override 33 | public void beforeJob(JobExecution jobExecution) { 34 | log.info("beforeJob"); 35 | } 36 | 37 | @Override 38 | public void afterJob(JobExecution jobExecution) { 39 | int sum = jobExecution.getStepExecutions().stream() 40 | .mapToInt(StepExecution::getWriteCount) 41 | .sum(); 42 | 43 | log.info("afterJob : {}", sum); 44 | } 45 | } 46 | 47 | public static class SavePersonAnnotationJobExecutionListener { 48 | 49 | @BeforeJob 50 | public void beforeJob(JobExecution jobExecution) { 51 | log.info("annotationBeforeJob"); 52 | } 53 | 54 | @AfterJob 55 | public void afterJob(JobExecution jobExecution) { 56 | int sum = jobExecution.getStepExecutions().stream() 57 | .mapToInt(StepExecution::getWriteCount) 58 | .sum(); 59 | 60 | log.info("annotationAfterJob : {}", sum); 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part4/LevelUpJobExecutionListener.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part4; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.batch.core.JobExecution; 5 | import org.springframework.batch.core.JobExecutionListener; 6 | 7 | import java.time.LocalDate; 8 | import java.util.Collection; 9 | 10 | @Slf4j 11 | public class LevelUpJobExecutionListener implements JobExecutionListener { 12 | 13 | private final UserRepository userRepository; 14 | 15 | public LevelUpJobExecutionListener(UserRepository userRepository) { 16 | this.userRepository = userRepository; 17 | } 18 | 19 | @Override 20 | public void beforeJob(JobExecution jobExecution) {} 21 | 22 | @Override 23 | public void afterJob(JobExecution jobExecution) { 24 | Collection users = userRepository.findAllByUpdatedDate(LocalDate.now()); 25 | 26 | long time = jobExecution.getEndTime().getTime() - jobExecution.getStartTime().getTime(); 27 | 28 | log.info("회원등급 업데이트 배치 프로그램"); 29 | log.info("-------------------------------"); 30 | log.info("총 데이터 처리 {}건, 처리 시간 {}millis", users.size(), time); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part4/SaveUserTasklet.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part4; 2 | 3 | import fastcampus.spring.batch.part5.Orders; 4 | import org.springframework.batch.core.StepContribution; 5 | import org.springframework.batch.core.scope.context.ChunkContext; 6 | import org.springframework.batch.core.step.tasklet.Tasklet; 7 | import org.springframework.batch.repeat.RepeatStatus; 8 | 9 | import java.time.LocalDate; 10 | import java.util.ArrayList; 11 | import java.util.Collections; 12 | import java.util.List; 13 | 14 | public class SaveUserTasklet implements Tasklet { 15 | 16 | private final int SIZE = 10_000; 17 | private final UserRepository userRepository; 18 | 19 | public SaveUserTasklet(UserRepository userRepository) { 20 | this.userRepository = userRepository; 21 | } 22 | 23 | @Override 24 | public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { 25 | List users = createUsers(); 26 | 27 | Collections.shuffle(users); 28 | 29 | userRepository.saveAll(users); 30 | 31 | return RepeatStatus.FINISHED; 32 | } 33 | 34 | private List createUsers() { 35 | List users = new ArrayList<>(); 36 | 37 | for (int i = 0; i < SIZE; i++) { 38 | users.add(User.builder() 39 | .orders(Collections.singletonList(Orders.builder() 40 | .amount(1_000) 41 | .createdDate(LocalDate.of(2020, 11, 1)) 42 | .itemName("item" + i) 43 | .build())) 44 | .username("test username" + i) 45 | .build()); 46 | } 47 | 48 | for (int i = 0; i < SIZE; i++) { 49 | users.add(User.builder() 50 | .orders(Collections.singletonList(Orders.builder() 51 | .amount(200_000) 52 | .createdDate(LocalDate.of(2020, 11, 2)) 53 | .itemName("item" + i) 54 | .build())) 55 | .username("test username" + i) 56 | .build()); 57 | } 58 | 59 | for (int i = 0; i < SIZE; i++) { 60 | users.add(User.builder() 61 | .orders(Collections.singletonList(Orders.builder() 62 | .amount(300_000) 63 | .createdDate(LocalDate.of(2020, 11, 3)) 64 | .itemName("item" + i) 65 | .build())) 66 | .username("test username" + i) 67 | .build()); 68 | } 69 | 70 | for (int i = 0; i < SIZE; i++) { 71 | users.add(User.builder() 72 | .orders(Collections.singletonList(Orders.builder() 73 | .amount(500_000) 74 | .createdDate(LocalDate.of(2020, 11, 4)) 75 | .itemName("item" + i) 76 | .build())) 77 | .username("test username" + i) 78 | .build()); 79 | } 80 | 81 | return users; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part4/User.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part4; 2 | 3 | import fastcampus.spring.batch.part5.Orders; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | import javax.persistence.*; 9 | import java.time.LocalDate; 10 | import java.util.List; 11 | import java.util.Objects; 12 | 13 | @Getter 14 | @Entity 15 | @NoArgsConstructor 16 | public class User { 17 | 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | private Long id; 21 | 22 | private String username; 23 | 24 | @Enumerated(EnumType.STRING) 25 | private Level level = Level.NORMAL; 26 | 27 | @OneToMany(cascade = CascadeType.PERSIST, fetch = FetchType.EAGER) 28 | @JoinColumn(name = "user_id") 29 | private List orders; 30 | 31 | private LocalDate updatedDate; 32 | 33 | @Builder 34 | private User(String username, List orders) { 35 | this.username = username; 36 | this.orders = orders; 37 | } 38 | 39 | public boolean availableLeveUp() { 40 | return Level.availableLevelUp(this.getLevel(), this.getTotalAmount()); 41 | } 42 | 43 | private int getTotalAmount() { 44 | return this.orders.stream() 45 | .mapToInt(Orders::getAmount) 46 | .sum(); 47 | } 48 | 49 | public Level levelUp() { 50 | Level nextLevel = Level.getNextLevel(this.getTotalAmount()); 51 | 52 | this.level = nextLevel; 53 | this.updatedDate = LocalDate.now(); 54 | 55 | return nextLevel; 56 | } 57 | 58 | public enum Level { 59 | VIP(500_000, null), 60 | GOLD(500_000, VIP), 61 | SILVER(300_000, GOLD), 62 | NORMAL(200_000, SILVER); 63 | 64 | private final int nextAmount; 65 | private final Level nextLevel; 66 | 67 | Level(int nextAmount, Level nextLevel) { 68 | this.nextAmount = nextAmount; 69 | this.nextLevel = nextLevel; 70 | } 71 | 72 | private static boolean availableLevelUp(Level level, int totalAmount) { 73 | if (Objects.isNull(level)) { 74 | return false; 75 | } 76 | 77 | if (Objects.isNull(level.nextLevel)) { 78 | return false; 79 | } 80 | 81 | return totalAmount >= level.nextAmount; 82 | } 83 | 84 | private static Level getNextLevel(int totalAmount) { 85 | if (totalAmount >= Level.VIP.nextAmount) { 86 | return VIP; 87 | } 88 | 89 | if (totalAmount >= Level.GOLD.nextAmount) { 90 | return GOLD.nextLevel; 91 | } 92 | 93 | if (totalAmount >= Level.SILVER.nextAmount) { 94 | return SILVER.nextLevel; 95 | } 96 | 97 | if (totalAmount >= Level.NORMAL.nextAmount) { 98 | return NORMAL.nextLevel; 99 | } 100 | 101 | return NORMAL; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part4/UserConfiguration.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part4; 2 | 3 | import fastcampus.spring.batch.part5.JobParametersDecide; 4 | import fastcampus.spring.batch.part5.OrderStatistics; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.batch.core.Job; 7 | import org.springframework.batch.core.Step; 8 | import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; 9 | import org.springframework.batch.core.configuration.annotation.JobScope; 10 | import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; 11 | import org.springframework.batch.core.launch.support.RunIdIncrementer; 12 | import org.springframework.batch.item.ItemProcessor; 13 | import org.springframework.batch.item.ItemReader; 14 | import org.springframework.batch.item.ItemWriter; 15 | import org.springframework.batch.item.database.JdbcPagingItemReader; 16 | import org.springframework.batch.item.database.JpaPagingItemReader; 17 | import org.springframework.batch.item.database.Order; 18 | import org.springframework.batch.item.database.builder.JdbcPagingItemReaderBuilder; 19 | import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder; 20 | import org.springframework.batch.item.file.FlatFileItemWriter; 21 | import org.springframework.batch.item.file.builder.FlatFileItemWriterBuilder; 22 | import org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor; 23 | import org.springframework.batch.item.file.transform.DelimitedLineAggregator; 24 | import org.springframework.beans.factory.annotation.Value; 25 | import org.springframework.context.annotation.Bean; 26 | import org.springframework.context.annotation.Configuration; 27 | import org.springframework.core.io.FileSystemResource; 28 | 29 | import javax.persistence.EntityManagerFactory; 30 | import javax.sql.DataSource; 31 | import java.time.LocalDate; 32 | import java.time.YearMonth; 33 | import java.time.format.DateTimeFormatter; 34 | import java.util.HashMap; 35 | import java.util.Map; 36 | 37 | @Configuration 38 | @Slf4j 39 | public class UserConfiguration { 40 | 41 | private final String JOB_NAME = "userJob"; 42 | private final int CHUNK = 1000; 43 | private final JobBuilderFactory jobBuilderFactory; 44 | private final StepBuilderFactory stepBuilderFactory; 45 | private final UserRepository userRepository; 46 | private final EntityManagerFactory entityManagerFactory; 47 | private final DataSource dataSource; 48 | 49 | public UserConfiguration(JobBuilderFactory jobBuilderFactory, 50 | StepBuilderFactory stepBuilderFactory, 51 | UserRepository userRepository, 52 | EntityManagerFactory entityManagerFactory, 53 | DataSource dataSource) { 54 | 55 | this.jobBuilderFactory = jobBuilderFactory; 56 | this.stepBuilderFactory = stepBuilderFactory; 57 | this.userRepository = userRepository; 58 | this.entityManagerFactory = entityManagerFactory; 59 | this.dataSource = dataSource; 60 | } 61 | 62 | @Bean(JOB_NAME) 63 | public Job userJob() throws Exception { 64 | return this.jobBuilderFactory.get(JOB_NAME) 65 | .incrementer(new RunIdIncrementer()) 66 | .start(this.saveUserStep()) 67 | .next(this.userLevelUpStep()) 68 | .listener(new LevelUpJobExecutionListener(userRepository)) 69 | .next(new JobParametersDecide("date")) 70 | .on(JobParametersDecide.CONTINUE.getName()) 71 | .to(this.orderStatisticsStep(null, null)) 72 | .build() 73 | .build(); 74 | } 75 | 76 | @Bean(JOB_NAME + "_orderStatisticsStep") 77 | @JobScope 78 | public Step orderStatisticsStep(@Value("#{jobParameters[date]}") String date, 79 | @Value("#{jobParameters[path]}") String path) throws Exception { 80 | return this.stepBuilderFactory.get(JOB_NAME + "_orderStatisticsStep") 81 | .chunk(CHUNK) 82 | .reader(orderStatisticsItemReader(date)) 83 | .writer(orderStatisticsItemWriter(date, path)) 84 | .build(); 85 | } 86 | 87 | private ItemWriter orderStatisticsItemWriter(String date, String path) throws Exception { 88 | YearMonth yearMonth = YearMonth.parse(date); 89 | String fileName = yearMonth.getYear() + "년_" + yearMonth.getMonthValue() + "월_일별_주문_금액.csv"; 90 | 91 | BeanWrapperFieldExtractor fieldExtractor = new BeanWrapperFieldExtractor<>(); 92 | fieldExtractor.setNames(new String[] {"amount", "date"}); 93 | 94 | DelimitedLineAggregator lineAggregator = new DelimitedLineAggregator<>(); 95 | lineAggregator.setDelimiter(","); 96 | lineAggregator.setFieldExtractor(fieldExtractor); 97 | 98 | FlatFileItemWriter itemWriter = new FlatFileItemWriterBuilder() 99 | .resource(new FileSystemResource(path + fileName)) 100 | .lineAggregator(lineAggregator) 101 | .name(JOB_NAME + "_orderStatisticsItemWriter") 102 | .encoding("UTF-8") 103 | .headerCallback(writer -> writer.write("total_amoun,date")) 104 | .build(); 105 | itemWriter.afterPropertiesSet(); 106 | 107 | return itemWriter; 108 | } 109 | 110 | private ItemReader orderStatisticsItemReader(String date) throws Exception { 111 | YearMonth yearMonth = YearMonth.parse(date); 112 | 113 | Map parameters = new HashMap<>(); 114 | parameters.put("startDate", yearMonth.atDay(1)); 115 | parameters.put("endDate", yearMonth.atEndOfMonth()); 116 | 117 | Map sortKey = new HashMap<>(); 118 | sortKey.put("created_date", Order.ASCENDING); 119 | 120 | JdbcPagingItemReader itemReader = new JdbcPagingItemReaderBuilder() 121 | .dataSource(this.dataSource) 122 | .rowMapper((resultSet, i) -> OrderStatistics.builder() 123 | .amount(resultSet.getString(1)) 124 | .date(LocalDate.parse(resultSet.getString(2), DateTimeFormatter.ISO_DATE)) 125 | .build()) 126 | .pageSize(CHUNK) 127 | .name(JOB_NAME + "_orderStatisticsItemReader") 128 | .selectClause("sum(amount), created_date") 129 | .fromClause("orders") 130 | .whereClause("created_date >= :startDate and created_date <= :endDate") 131 | .groupClause("created_date") 132 | .parameterValues(parameters) 133 | .sortKeys(sortKey) 134 | .build(); 135 | itemReader.afterPropertiesSet(); 136 | return itemReader; 137 | } 138 | 139 | @Bean(JOB_NAME + "_saveUserStep") 140 | public Step saveUserStep() { 141 | return this.stepBuilderFactory.get(JOB_NAME + "_saveUserStep") 142 | .tasklet(new SaveUserTasklet(userRepository)) 143 | .build(); 144 | } 145 | 146 | @Bean(JOB_NAME + "_userLevelUpStep") 147 | public Step userLevelUpStep() throws Exception { 148 | return this.stepBuilderFactory.get(JOB_NAME + "_userLevelUpStep") 149 | .chunk(CHUNK) 150 | .reader(itemReader()) 151 | .processor(itemProcessor()) 152 | .writer(itemWriter()) 153 | .build(); 154 | } 155 | 156 | private ItemWriter itemWriter() { 157 | return users -> users.forEach(x -> { 158 | x.levelUp(); 159 | userRepository.save(x); 160 | }); 161 | } 162 | 163 | private ItemProcessor itemProcessor() { 164 | return user -> { 165 | if (user.availableLeveUp()) { 166 | return user; 167 | } 168 | 169 | return null; 170 | }; 171 | } 172 | 173 | private ItemReader itemReader() throws Exception { 174 | JpaPagingItemReader itemReader = new JpaPagingItemReaderBuilder() 175 | .queryString("select u from User u") 176 | .entityManagerFactory(entityManagerFactory) 177 | .pageSize(CHUNK) 178 | .name(JOB_NAME + "_userItemReader") 179 | .build(); 180 | 181 | itemReader.afterPropertiesSet(); 182 | 183 | return itemReader; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part4/UserRepository.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part4; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.jpa.repository.Query; 5 | 6 | import java.time.LocalDate; 7 | import java.util.Collection; 8 | 9 | public interface UserRepository extends JpaRepository { 10 | Collection findAllByUpdatedDate(LocalDate updatedDate); 11 | 12 | 13 | @Query(value = "select min(u.id) from User u") 14 | long findMinId(); 15 | 16 | @Query(value = "select max(u.id) from User u") 17 | long findMaxId(); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part5/JobParametersDecide.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part5; 2 | 3 | import io.micrometer.core.instrument.util.StringUtils; 4 | import org.springframework.batch.core.JobExecution; 5 | import org.springframework.batch.core.StepExecution; 6 | import org.springframework.batch.core.job.flow.FlowExecutionStatus; 7 | import org.springframework.batch.core.job.flow.JobExecutionDecider; 8 | 9 | public class JobParametersDecide implements JobExecutionDecider { 10 | 11 | public static final FlowExecutionStatus CONTINUE = new FlowExecutionStatus("CONTINUE"); 12 | 13 | private final String key; 14 | 15 | public JobParametersDecide(String key) { 16 | this.key = key; 17 | } 18 | 19 | @Override 20 | public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) { 21 | String value = jobExecution.getJobParameters().getString(key); 22 | 23 | if (StringUtils.isEmpty(value)) { 24 | return FlowExecutionStatus.COMPLETED; 25 | } 26 | 27 | return CONTINUE; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part5/OrderStatistics.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part5; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | 6 | import java.time.LocalDate; 7 | 8 | @Getter 9 | public class OrderStatistics { 10 | 11 | private String amount; 12 | 13 | private LocalDate date; 14 | 15 | @Builder 16 | private OrderStatistics(String amount, LocalDate date) { 17 | this.amount = amount; 18 | this.date = date; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part5/Orders.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part5; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | import javax.persistence.Entity; 8 | import javax.persistence.GeneratedValue; 9 | import javax.persistence.GenerationType; 10 | import javax.persistence.Id; 11 | import java.time.LocalDate; 12 | 13 | @Entity 14 | @Getter 15 | @NoArgsConstructor 16 | public class Orders { 17 | 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | private Long id; 21 | 22 | private String itemName; 23 | 24 | private int amount; 25 | 26 | private LocalDate createdDate; 27 | 28 | @Builder 29 | private Orders(String itemName, int amount, LocalDate createdDate) { 30 | this.itemName = itemName; 31 | this.amount = amount; 32 | this.createdDate = createdDate; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part6/AsyncUserConfiguration.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part6; 2 | 3 | import fastcampus.spring.batch.part4.LevelUpJobExecutionListener; 4 | import fastcampus.spring.batch.part4.SaveUserTasklet; 5 | import fastcampus.spring.batch.part4.User; 6 | import fastcampus.spring.batch.part4.UserRepository; 7 | import fastcampus.spring.batch.part5.JobParametersDecide; 8 | import fastcampus.spring.batch.part5.OrderStatistics; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.batch.core.Job; 11 | import org.springframework.batch.core.Step; 12 | import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; 13 | import org.springframework.batch.core.configuration.annotation.JobScope; 14 | import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; 15 | import org.springframework.batch.core.launch.support.RunIdIncrementer; 16 | import org.springframework.batch.integration.async.AsyncItemProcessor; 17 | import org.springframework.batch.integration.async.AsyncItemWriter; 18 | import org.springframework.batch.item.ItemProcessor; 19 | import org.springframework.batch.item.ItemReader; 20 | import org.springframework.batch.item.ItemWriter; 21 | import org.springframework.batch.item.database.JdbcPagingItemReader; 22 | import org.springframework.batch.item.database.JpaPagingItemReader; 23 | import org.springframework.batch.item.database.Order; 24 | import org.springframework.batch.item.database.builder.JdbcPagingItemReaderBuilder; 25 | import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder; 26 | import org.springframework.batch.item.file.FlatFileItemWriter; 27 | import org.springframework.batch.item.file.builder.FlatFileItemWriterBuilder; 28 | import org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor; 29 | import org.springframework.batch.item.file.transform.DelimitedLineAggregator; 30 | import org.springframework.beans.factory.annotation.Value; 31 | import org.springframework.context.annotation.Bean; 32 | import org.springframework.context.annotation.Configuration; 33 | import org.springframework.core.io.FileSystemResource; 34 | import org.springframework.core.task.TaskExecutor; 35 | 36 | import javax.persistence.EntityManagerFactory; 37 | import javax.sql.DataSource; 38 | import java.time.LocalDate; 39 | import java.time.YearMonth; 40 | import java.time.format.DateTimeFormatter; 41 | import java.util.HashMap; 42 | import java.util.Map; 43 | import java.util.concurrent.Future; 44 | 45 | @Configuration 46 | @Slf4j 47 | public class AsyncUserConfiguration { 48 | 49 | private final String JOB_NAME = "asyncUserJob"; 50 | private final int CHUNK = 1000; 51 | private final JobBuilderFactory jobBuilderFactory; 52 | private final StepBuilderFactory stepBuilderFactory; 53 | private final UserRepository userRepository; 54 | private final EntityManagerFactory entityManagerFactory; 55 | private final DataSource dataSource; 56 | private final TaskExecutor taskExecutor; 57 | 58 | public AsyncUserConfiguration(JobBuilderFactory jobBuilderFactory, 59 | StepBuilderFactory stepBuilderFactory, 60 | UserRepository userRepository, 61 | EntityManagerFactory entityManagerFactory, 62 | DataSource dataSource, 63 | TaskExecutor taskExecutor) { 64 | 65 | this.jobBuilderFactory = jobBuilderFactory; 66 | this.stepBuilderFactory = stepBuilderFactory; 67 | this.userRepository = userRepository; 68 | this.entityManagerFactory = entityManagerFactory; 69 | this.dataSource = dataSource; 70 | this.taskExecutor = taskExecutor; 71 | } 72 | 73 | @Bean(JOB_NAME) 74 | public Job userJob() throws Exception { 75 | return this.jobBuilderFactory.get(JOB_NAME) 76 | .incrementer(new RunIdIncrementer()) 77 | .start(this.saveUserStep()) 78 | .next(this.userLevelUpStep()) 79 | .listener(new LevelUpJobExecutionListener(userRepository)) 80 | .next(new JobParametersDecide("date")) 81 | .on(JobParametersDecide.CONTINUE.getName()) 82 | .to(this.orderStatisticsStep(null)) 83 | .build() 84 | .build(); 85 | } 86 | 87 | @Bean(JOB_NAME + "_orderStatisticsStep") 88 | @JobScope 89 | public Step orderStatisticsStep(@Value("#{jobParameters[date]}") String date) throws Exception { 90 | return this.stepBuilderFactory.get(JOB_NAME + "_orderStatisticsStep") 91 | .chunk(CHUNK) 92 | .reader(orderStatisticsItemReader(date)) 93 | .writer(orderStatisticsItemWriter(date)) 94 | .build(); 95 | } 96 | 97 | private ItemWriter orderStatisticsItemWriter(String date) throws Exception { 98 | YearMonth yearMonth = YearMonth.parse(date); 99 | String fileName = yearMonth.getYear() + "년_" + yearMonth.getMonthValue() + "월_일별_주문_금액.csv"; 100 | 101 | BeanWrapperFieldExtractor fieldExtractor = new BeanWrapperFieldExtractor<>(); 102 | fieldExtractor.setNames(new String[] {"amount", "date"}); 103 | 104 | DelimitedLineAggregator lineAggregator = new DelimitedLineAggregator<>(); 105 | lineAggregator.setDelimiter(","); 106 | lineAggregator.setFieldExtractor(fieldExtractor); 107 | 108 | FlatFileItemWriter itemWriter = new FlatFileItemWriterBuilder() 109 | .resource(new FileSystemResource("output/" + fileName)) 110 | .lineAggregator(lineAggregator) 111 | .name(JOB_NAME + "_orderStatisticsItemWriter") 112 | .encoding("UTF-8") 113 | .headerCallback(writer -> writer.write("total_amoun,date")) 114 | .build(); 115 | itemWriter.afterPropertiesSet(); 116 | 117 | return itemWriter; 118 | } 119 | 120 | private ItemReader orderStatisticsItemReader(String date) throws Exception { 121 | YearMonth yearMonth = YearMonth.parse(date); 122 | 123 | Map parameters = new HashMap<>(); 124 | parameters.put("startDate", yearMonth.atDay(1)); 125 | parameters.put("endDate", yearMonth.atEndOfMonth()); 126 | 127 | Map sortKey = new HashMap<>(); 128 | sortKey.put("created_date", Order.ASCENDING); 129 | 130 | JdbcPagingItemReader itemReader = new JdbcPagingItemReaderBuilder() 131 | .dataSource(this.dataSource) 132 | .rowMapper((resultSet, i) -> OrderStatistics.builder() 133 | .amount(resultSet.getString(1)) 134 | .date(LocalDate.parse(resultSet.getString(2), DateTimeFormatter.ISO_DATE)) 135 | .build()) 136 | .pageSize(CHUNK) 137 | .name(JOB_NAME + "_orderStatisticsItemReader") 138 | .selectClause("sum(amount), created_date") 139 | .fromClause("orders") 140 | .whereClause("created_date >= :startDate and created_date <= :endDate") 141 | .groupClause("created_date") 142 | .parameterValues(parameters) 143 | .sortKeys(sortKey) 144 | .build(); 145 | itemReader.afterPropertiesSet(); 146 | return itemReader; 147 | } 148 | 149 | @Bean(JOB_NAME + "_saveUserStep") 150 | public Step saveUserStep() { 151 | return this.stepBuilderFactory.get(JOB_NAME + "_saveUserStep") 152 | .tasklet(new SaveUserTasklet(userRepository)) 153 | .build(); 154 | } 155 | 156 | @Bean(JOB_NAME + "_userLevelUpStep") 157 | public Step userLevelUpStep() throws Exception { 158 | return this.stepBuilderFactory.get(JOB_NAME + "_userLevelUpStep") 159 | .>chunk(CHUNK) 160 | .reader(itemReader()) 161 | .processor(itemProcessor()) 162 | .writer(itemWriter()) 163 | .build(); 164 | } 165 | 166 | private AsyncItemWriter itemWriter() { 167 | ItemWriter itemWriter = users -> users.forEach(x -> { 168 | x.levelUp(); 169 | userRepository.save(x); 170 | }); 171 | 172 | AsyncItemWriter asyncItemWriter = new AsyncItemWriter<>(); 173 | asyncItemWriter.setDelegate(itemWriter); 174 | 175 | return asyncItemWriter; 176 | } 177 | 178 | private AsyncItemProcessor itemProcessor() { 179 | ItemProcessor itemProcessor = user -> { 180 | if (user.availableLeveUp()) { 181 | return user; 182 | } 183 | 184 | return null; 185 | }; 186 | 187 | AsyncItemProcessor asyncItemProcessor = new AsyncItemProcessor<>(); 188 | asyncItemProcessor.setDelegate(itemProcessor); 189 | asyncItemProcessor.setTaskExecutor(this.taskExecutor); 190 | 191 | return asyncItemProcessor; 192 | } 193 | 194 | private ItemReader itemReader() throws Exception { 195 | JpaPagingItemReader itemReader = new JpaPagingItemReaderBuilder() 196 | .queryString("select u from User u") 197 | .entityManagerFactory(entityManagerFactory) 198 | .pageSize(CHUNK) 199 | .name(JOB_NAME + "_userItemReader") 200 | .build(); 201 | 202 | itemReader.afterPropertiesSet(); 203 | 204 | return itemReader; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part6/MultiThreadUserConfiguration.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part6; 2 | 3 | import fastcampus.spring.batch.part4.LevelUpJobExecutionListener; 4 | import fastcampus.spring.batch.part4.SaveUserTasklet; 5 | import fastcampus.spring.batch.part4.User; 6 | import fastcampus.spring.batch.part4.UserRepository; 7 | import fastcampus.spring.batch.part5.JobParametersDecide; 8 | import fastcampus.spring.batch.part5.OrderStatistics; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.batch.core.Job; 11 | import org.springframework.batch.core.Step; 12 | import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; 13 | import org.springframework.batch.core.configuration.annotation.JobScope; 14 | import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; 15 | import org.springframework.batch.core.launch.support.RunIdIncrementer; 16 | import org.springframework.batch.item.ItemProcessor; 17 | import org.springframework.batch.item.ItemReader; 18 | import org.springframework.batch.item.ItemWriter; 19 | import org.springframework.batch.item.database.JdbcPagingItemReader; 20 | import org.springframework.batch.item.database.JpaPagingItemReader; 21 | import org.springframework.batch.item.database.Order; 22 | import org.springframework.batch.item.database.builder.JdbcPagingItemReaderBuilder; 23 | import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder; 24 | import org.springframework.batch.item.file.FlatFileItemWriter; 25 | import org.springframework.batch.item.file.builder.FlatFileItemWriterBuilder; 26 | import org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor; 27 | import org.springframework.batch.item.file.transform.DelimitedLineAggregator; 28 | import org.springframework.beans.factory.annotation.Value; 29 | import org.springframework.context.annotation.Bean; 30 | import org.springframework.context.annotation.Configuration; 31 | import org.springframework.core.io.FileSystemResource; 32 | import org.springframework.core.task.TaskExecutor; 33 | 34 | import javax.persistence.EntityManagerFactory; 35 | import javax.sql.DataSource; 36 | import java.time.LocalDate; 37 | import java.time.YearMonth; 38 | import java.time.format.DateTimeFormatter; 39 | import java.util.HashMap; 40 | import java.util.Map; 41 | 42 | @Configuration 43 | @Slf4j 44 | public class MultiThreadUserConfiguration { 45 | 46 | private final String JOB_NAME = "multiThreadUserJob"; 47 | private final int CHUNK = 1000; 48 | private final JobBuilderFactory jobBuilderFactory; 49 | private final StepBuilderFactory stepBuilderFactory; 50 | private final UserRepository userRepository; 51 | private final EntityManagerFactory entityManagerFactory; 52 | private final DataSource dataSource; 53 | private final TaskExecutor taskExecutor; 54 | 55 | public MultiThreadUserConfiguration(JobBuilderFactory jobBuilderFactory, 56 | StepBuilderFactory stepBuilderFactory, 57 | UserRepository userRepository, 58 | EntityManagerFactory entityManagerFactory, 59 | DataSource dataSource, TaskExecutor taskExecutor) { 60 | 61 | this.jobBuilderFactory = jobBuilderFactory; 62 | this.stepBuilderFactory = stepBuilderFactory; 63 | this.userRepository = userRepository; 64 | this.entityManagerFactory = entityManagerFactory; 65 | this.dataSource = dataSource; 66 | this.taskExecutor = taskExecutor; 67 | } 68 | 69 | @Bean(JOB_NAME) 70 | public Job userJob() throws Exception { 71 | return this.jobBuilderFactory.get(JOB_NAME) 72 | .incrementer(new RunIdIncrementer()) 73 | .start(this.saveUserStep()) 74 | .next(this.userLevelUpStep()) 75 | .listener(new LevelUpJobExecutionListener(userRepository)) 76 | .next(new JobParametersDecide("date")) 77 | .on(JobParametersDecide.CONTINUE.getName()) 78 | .to(this.orderStatisticsStep(null)) 79 | .build() 80 | .build(); 81 | } 82 | 83 | @Bean(JOB_NAME + "_orderStatisticsStep") 84 | @JobScope 85 | public Step orderStatisticsStep(@Value("#{jobParameters[date]}") String date) throws Exception { 86 | return this.stepBuilderFactory.get(JOB_NAME + "_orderStatisticsStep") 87 | .chunk(CHUNK) 88 | .reader(orderStatisticsItemReader(date)) 89 | .writer(orderStatisticsItemWriter(date)) 90 | .build(); 91 | } 92 | 93 | private ItemWriter orderStatisticsItemWriter(String date) throws Exception { 94 | YearMonth yearMonth = YearMonth.parse(date); 95 | String fileName = yearMonth.getYear() + "년_" + yearMonth.getMonthValue() + "월_일별_주문_금액.csv"; 96 | 97 | BeanWrapperFieldExtractor fieldExtractor = new BeanWrapperFieldExtractor<>(); 98 | fieldExtractor.setNames(new String[] {"amount", "date"}); 99 | 100 | DelimitedLineAggregator lineAggregator = new DelimitedLineAggregator<>(); 101 | lineAggregator.setDelimiter(","); 102 | lineAggregator.setFieldExtractor(fieldExtractor); 103 | 104 | FlatFileItemWriter itemWriter = new FlatFileItemWriterBuilder() 105 | .resource(new FileSystemResource("output/" + fileName)) 106 | .lineAggregator(lineAggregator) 107 | .name(JOB_NAME + "_orderStatisticsItemWriter") 108 | .encoding("UTF-8") 109 | .headerCallback(writer -> writer.write("total_amoun,date")) 110 | .build(); 111 | itemWriter.afterPropertiesSet(); 112 | 113 | return itemWriter; 114 | } 115 | 116 | private ItemReader orderStatisticsItemReader(String date) throws Exception { 117 | YearMonth yearMonth = YearMonth.parse(date); 118 | 119 | Map parameters = new HashMap<>(); 120 | parameters.put("startDate", yearMonth.atDay(1)); 121 | parameters.put("endDate", yearMonth.atEndOfMonth()); 122 | 123 | Map sortKey = new HashMap<>(); 124 | sortKey.put("created_date", Order.ASCENDING); 125 | 126 | JdbcPagingItemReader itemReader = new JdbcPagingItemReaderBuilder() 127 | .dataSource(this.dataSource) 128 | .rowMapper((resultSet, i) -> OrderStatistics.builder() 129 | .amount(resultSet.getString(1)) 130 | .date(LocalDate.parse(resultSet.getString(2), DateTimeFormatter.ISO_DATE)) 131 | .build()) 132 | .pageSize(CHUNK) 133 | .name(JOB_NAME + "_orderStatisticsItemReader") 134 | .selectClause("sum(amount), created_date") 135 | .fromClause("orders") 136 | .whereClause("created_date >= :startDate and created_date <= :endDate") 137 | .groupClause("created_date") 138 | .parameterValues(parameters) 139 | .sortKeys(sortKey) 140 | .build(); 141 | itemReader.afterPropertiesSet(); 142 | return itemReader; 143 | } 144 | 145 | @Bean(JOB_NAME + "_saveUserStep") 146 | public Step saveUserStep() { 147 | return this.stepBuilderFactory.get(JOB_NAME + "_saveUserStep") 148 | .tasklet(new SaveUserTasklet(userRepository)) 149 | .build(); 150 | } 151 | 152 | @Bean(JOB_NAME + "_userLevelUpStep") 153 | public Step userLevelUpStep() throws Exception { 154 | return this.stepBuilderFactory.get(JOB_NAME + "_userLevelUpStep") 155 | .chunk(CHUNK) 156 | .reader(itemReader()) 157 | .processor(itemProcessor()) 158 | .writer(itemWriter()) 159 | .taskExecutor(this.taskExecutor) 160 | .throttleLimit(8) 161 | .build(); 162 | } 163 | 164 | private ItemWriter itemWriter() { 165 | return users -> users.forEach(x -> { 166 | x.levelUp(); 167 | userRepository.save(x); 168 | }); 169 | } 170 | 171 | private ItemProcessor itemProcessor() { 172 | return user -> { 173 | if (user.availableLeveUp()) { 174 | return user; 175 | } 176 | 177 | return null; 178 | }; 179 | } 180 | 181 | private ItemReader itemReader() throws Exception { 182 | JpaPagingItemReader itemReader = new JpaPagingItemReaderBuilder() 183 | .queryString("select u from User u") 184 | .entityManagerFactory(entityManagerFactory) 185 | .pageSize(CHUNK) 186 | .name(JOB_NAME + "_userItemReader") 187 | .build(); 188 | 189 | itemReader.afterPropertiesSet(); 190 | 191 | return itemReader; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part6/ParallelUserConfiguration.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part6; 2 | 3 | import fastcampus.spring.batch.part4.LevelUpJobExecutionListener; 4 | import fastcampus.spring.batch.part4.SaveUserTasklet; 5 | import fastcampus.spring.batch.part4.User; 6 | import fastcampus.spring.batch.part4.UserRepository; 7 | import fastcampus.spring.batch.part5.JobParametersDecide; 8 | import fastcampus.spring.batch.part5.OrderStatistics; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.batch.core.Job; 11 | import org.springframework.batch.core.Step; 12 | import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; 13 | import org.springframework.batch.core.configuration.annotation.JobScope; 14 | import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; 15 | import org.springframework.batch.core.configuration.annotation.StepScope; 16 | import org.springframework.batch.core.job.builder.FlowBuilder; 17 | import org.springframework.batch.core.job.flow.Flow; 18 | import org.springframework.batch.core.job.flow.support.SimpleFlow; 19 | import org.springframework.batch.core.launch.support.RunIdIncrementer; 20 | import org.springframework.batch.core.partition.PartitionHandler; 21 | import org.springframework.batch.core.partition.support.TaskExecutorPartitionHandler; 22 | import org.springframework.batch.core.step.tasklet.TaskletStep; 23 | import org.springframework.batch.item.ItemProcessor; 24 | import org.springframework.batch.item.ItemReader; 25 | import org.springframework.batch.item.ItemWriter; 26 | import org.springframework.batch.item.database.JdbcPagingItemReader; 27 | import org.springframework.batch.item.database.JpaPagingItemReader; 28 | import org.springframework.batch.item.database.Order; 29 | import org.springframework.batch.item.database.builder.JdbcPagingItemReaderBuilder; 30 | import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder; 31 | import org.springframework.batch.item.file.FlatFileItemWriter; 32 | import org.springframework.batch.item.file.builder.FlatFileItemWriterBuilder; 33 | import org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor; 34 | import org.springframework.batch.item.file.transform.DelimitedLineAggregator; 35 | import org.springframework.beans.factory.annotation.Value; 36 | import org.springframework.context.annotation.Bean; 37 | import org.springframework.context.annotation.Configuration; 38 | import org.springframework.core.io.FileSystemResource; 39 | import org.springframework.core.task.TaskExecutor; 40 | 41 | import javax.persistence.EntityManagerFactory; 42 | import javax.sql.DataSource; 43 | import java.time.LocalDate; 44 | import java.time.YearMonth; 45 | import java.time.format.DateTimeFormatter; 46 | import java.util.HashMap; 47 | import java.util.Map; 48 | 49 | @Configuration 50 | @Slf4j 51 | public class ParallelUserConfiguration { 52 | 53 | private final String JOB_NAME = "parallelUserJob"; 54 | private final int CHUNK = 1000; 55 | private final JobBuilderFactory jobBuilderFactory; 56 | private final StepBuilderFactory stepBuilderFactory; 57 | private final UserRepository userRepository; 58 | private final EntityManagerFactory entityManagerFactory; 59 | private final DataSource dataSource; 60 | private final TaskExecutor taskExecutor; 61 | 62 | public ParallelUserConfiguration(JobBuilderFactory jobBuilderFactory, 63 | StepBuilderFactory stepBuilderFactory, 64 | UserRepository userRepository, 65 | EntityManagerFactory entityManagerFactory, 66 | DataSource dataSource, TaskExecutor taskExecutor) { 67 | 68 | this.jobBuilderFactory = jobBuilderFactory; 69 | this.stepBuilderFactory = stepBuilderFactory; 70 | this.userRepository = userRepository; 71 | this.entityManagerFactory = entityManagerFactory; 72 | this.dataSource = dataSource; 73 | this.taskExecutor = taskExecutor; 74 | } 75 | 76 | @Bean(JOB_NAME) 77 | public Job userJob() throws Exception { 78 | return this.jobBuilderFactory.get(JOB_NAME) 79 | .incrementer(new RunIdIncrementer()) 80 | .listener(new LevelUpJobExecutionListener(userRepository)) 81 | .start(this.saveUserFlow()) 82 | .next(this.splitFlow(null)) 83 | .build() 84 | .build(); 85 | } 86 | 87 | @Bean(JOB_NAME + "_saveUserFlow") 88 | public Flow saveUserFlow() { 89 | TaskletStep saveUserStep = this.stepBuilderFactory.get(JOB_NAME + "_saveUserStep") 90 | .tasklet(new SaveUserTasklet(userRepository)) 91 | .build(); 92 | 93 | return new FlowBuilder(JOB_NAME + "_saveUserFlow") 94 | .start(saveUserStep) 95 | .build(); 96 | } 97 | 98 | @Bean(JOB_NAME + "_splitFlow") 99 | @JobScope 100 | public Flow splitFlow(@Value("#{jobParameters[date]}") String date) throws Exception { 101 | Flow userLevelUpFlow = new FlowBuilder(JOB_NAME + "_userLevelUpFlow") 102 | .start(userLevelUpManagerStep()) 103 | .build(); 104 | 105 | return new FlowBuilder(JOB_NAME + "_splitFlow") 106 | .split(this.taskExecutor) 107 | .add(userLevelUpFlow, orderStatisticsFlow(date)) 108 | .build(); 109 | 110 | } 111 | 112 | private Flow orderStatisticsFlow(String date) throws Exception { 113 | return new FlowBuilder(JOB_NAME + "_orderStatisticsFlow") 114 | .start(new JobParametersDecide("date")) 115 | .on(JobParametersDecide.CONTINUE.getName()) 116 | .to(this.orderStatisticsStep(date)) 117 | .build(); 118 | } 119 | 120 | private Step orderStatisticsStep(@Value("#{jobParameters[date]}") String date) throws Exception { 121 | return this.stepBuilderFactory.get(JOB_NAME + "_orderStatisticsStep") 122 | .chunk(CHUNK) 123 | .reader(orderStatisticsItemReader(date)) 124 | .writer(orderStatisticsItemWriter(date)) 125 | .build(); 126 | } 127 | 128 | private ItemWriter orderStatisticsItemWriter(String date) throws Exception { 129 | YearMonth yearMonth = YearMonth.parse(date); 130 | String fileName = yearMonth.getYear() + "년_" + yearMonth.getMonthValue() + "월_일별_주문_금액.csv"; 131 | 132 | BeanWrapperFieldExtractor fieldExtractor = new BeanWrapperFieldExtractor<>(); 133 | fieldExtractor.setNames(new String[] {"amount", "date"}); 134 | 135 | DelimitedLineAggregator lineAggregator = new DelimitedLineAggregator<>(); 136 | lineAggregator.setDelimiter(","); 137 | lineAggregator.setFieldExtractor(fieldExtractor); 138 | 139 | FlatFileItemWriter itemWriter = new FlatFileItemWriterBuilder() 140 | .resource(new FileSystemResource("output/" + fileName)) 141 | .lineAggregator(lineAggregator) 142 | .name(JOB_NAME + "_orderStatisticsItemWriter") 143 | .encoding("UTF-8") 144 | .headerCallback(writer -> writer.write("total_amoun,date")) 145 | .build(); 146 | itemWriter.afterPropertiesSet(); 147 | 148 | return itemWriter; 149 | } 150 | 151 | private ItemReader orderStatisticsItemReader(String date) throws Exception { 152 | YearMonth yearMonth = YearMonth.parse(date); 153 | 154 | Map parameters = new HashMap<>(); 155 | parameters.put("startDate", yearMonth.atDay(1)); 156 | parameters.put("endDate", yearMonth.atEndOfMonth()); 157 | 158 | Map sortKey = new HashMap<>(); 159 | sortKey.put("created_date", Order.ASCENDING); 160 | 161 | JdbcPagingItemReader itemReader = new JdbcPagingItemReaderBuilder() 162 | .dataSource(this.dataSource) 163 | .rowMapper((resultSet, i) -> OrderStatistics.builder() 164 | .amount(resultSet.getString(1)) 165 | .date(LocalDate.parse(resultSet.getString(2), DateTimeFormatter.ISO_DATE)) 166 | .build()) 167 | .pageSize(CHUNK) 168 | .name(JOB_NAME + "_orderStatisticsItemReader") 169 | .selectClause("sum(amount), created_date") 170 | .fromClause("orders") 171 | .whereClause("created_date >= :startDate and created_date <= :endDate") 172 | .groupClause("created_date") 173 | .parameterValues(parameters) 174 | .sortKeys(sortKey) 175 | .build(); 176 | itemReader.afterPropertiesSet(); 177 | return itemReader; 178 | } 179 | 180 | @Bean(JOB_NAME + "_userLevelUpStep") 181 | public Step userLevelUpStep() throws Exception { 182 | return this.stepBuilderFactory.get(JOB_NAME + "_userLevelUpStep") 183 | .chunk(CHUNK) 184 | .reader(itemReader(null, null)) 185 | .processor(itemProcessor()) 186 | .writer(itemWriter()) 187 | .build(); 188 | } 189 | 190 | private ItemWriter itemWriter() { 191 | return users -> users.forEach(x -> { 192 | x.levelUp(); 193 | userRepository.save(x); 194 | }); 195 | } 196 | 197 | private ItemProcessor itemProcessor() { 198 | return user -> { 199 | if (user.availableLeveUp()) { 200 | return user; 201 | } 202 | 203 | return null; 204 | }; 205 | } 206 | 207 | @Bean(JOB_NAME + "_userLevelUpStep.manager") 208 | public Step userLevelUpManagerStep() throws Exception { 209 | return this.stepBuilderFactory.get(JOB_NAME + "_userLevelUpStep.manager") 210 | .partitioner(JOB_NAME + "_userLevelUpStep", new UserLevelUpPartitioner(userRepository)) 211 | .step(userLevelUpStep()) 212 | .partitionHandler(taskExecutorPartitionHandler()) 213 | .build(); 214 | } 215 | 216 | @Bean(JOB_NAME + "_taskExecutorPartitionHandler") 217 | PartitionHandler taskExecutorPartitionHandler() throws Exception { 218 | TaskExecutorPartitionHandler handler = new TaskExecutorPartitionHandler(); 219 | handler.setStep(userLevelUpStep()); 220 | handler.setTaskExecutor(this.taskExecutor); 221 | handler.setGridSize(8); 222 | 223 | return handler; 224 | } 225 | 226 | @Bean(JOB_NAME + "_userItemReader") 227 | @StepScope 228 | JpaPagingItemReader itemReader(@Value("#{stepExecutionContext[minId]}") Long minId, 229 | @Value("#{stepExecutionContext[maxId]}") Long maxId) throws Exception { 230 | 231 | Map parameters = new HashMap<>(); 232 | parameters.put("minId", minId); 233 | parameters.put("maxId", maxId); 234 | 235 | JpaPagingItemReader itemReader = new JpaPagingItemReaderBuilder() 236 | .queryString("select u from User u where u.id between :minId and :maxId") 237 | .parameterValues(parameters) 238 | .entityManagerFactory(entityManagerFactory) 239 | .pageSize(CHUNK) 240 | .name(JOB_NAME + "_userItemReader") 241 | .build(); 242 | 243 | itemReader.afterPropertiesSet(); 244 | 245 | return itemReader; 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part6/PartitionUserConfiguration.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part6; 2 | 3 | import fastcampus.spring.batch.part4.LevelUpJobExecutionListener; 4 | import fastcampus.spring.batch.part4.SaveUserTasklet; 5 | import fastcampus.spring.batch.part4.User; 6 | import fastcampus.spring.batch.part4.UserRepository; 7 | import fastcampus.spring.batch.part5.JobParametersDecide; 8 | import fastcampus.spring.batch.part5.OrderStatistics; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.batch.core.Job; 11 | import org.springframework.batch.core.Step; 12 | import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; 13 | import org.springframework.batch.core.configuration.annotation.JobScope; 14 | import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; 15 | import org.springframework.batch.core.configuration.annotation.StepScope; 16 | import org.springframework.batch.core.launch.support.RunIdIncrementer; 17 | import org.springframework.batch.core.partition.PartitionHandler; 18 | import org.springframework.batch.core.partition.support.TaskExecutorPartitionHandler; 19 | import org.springframework.batch.integration.async.AsyncItemProcessor; 20 | import org.springframework.batch.integration.async.AsyncItemWriter; 21 | import org.springframework.batch.item.ItemProcessor; 22 | import org.springframework.batch.item.ItemReader; 23 | import org.springframework.batch.item.ItemWriter; 24 | import org.springframework.batch.item.database.JdbcPagingItemReader; 25 | import org.springframework.batch.item.database.JpaPagingItemReader; 26 | import org.springframework.batch.item.database.Order; 27 | import org.springframework.batch.item.database.builder.JdbcPagingItemReaderBuilder; 28 | import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder; 29 | import org.springframework.batch.item.file.FlatFileItemWriter; 30 | import org.springframework.batch.item.file.builder.FlatFileItemWriterBuilder; 31 | import org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor; 32 | import org.springframework.batch.item.file.transform.DelimitedLineAggregator; 33 | import org.springframework.beans.factory.annotation.Value; 34 | import org.springframework.context.annotation.Bean; 35 | import org.springframework.context.annotation.Configuration; 36 | import org.springframework.core.io.FileSystemResource; 37 | import org.springframework.core.task.TaskExecutor; 38 | 39 | import javax.persistence.EntityManagerFactory; 40 | import javax.sql.DataSource; 41 | import java.time.LocalDate; 42 | import java.time.YearMonth; 43 | import java.time.format.DateTimeFormatter; 44 | import java.util.HashMap; 45 | import java.util.Map; 46 | import java.util.concurrent.Future; 47 | 48 | @Configuration 49 | @Slf4j 50 | public class PartitionUserConfiguration { 51 | 52 | private final String JOB_NAME = "partitionUserJob"; 53 | private final int CHUNK = 1000; 54 | private final JobBuilderFactory jobBuilderFactory; 55 | private final StepBuilderFactory stepBuilderFactory; 56 | private final UserRepository userRepository; 57 | private final EntityManagerFactory entityManagerFactory; 58 | private final DataSource dataSource; 59 | private final TaskExecutor taskExecutor; 60 | 61 | public PartitionUserConfiguration(JobBuilderFactory jobBuilderFactory, 62 | StepBuilderFactory stepBuilderFactory, 63 | UserRepository userRepository, 64 | EntityManagerFactory entityManagerFactory, 65 | DataSource dataSource, TaskExecutor taskExecutor) { 66 | 67 | this.jobBuilderFactory = jobBuilderFactory; 68 | this.stepBuilderFactory = stepBuilderFactory; 69 | this.userRepository = userRepository; 70 | this.entityManagerFactory = entityManagerFactory; 71 | this.dataSource = dataSource; 72 | this.taskExecutor = taskExecutor; 73 | } 74 | 75 | @Bean(JOB_NAME) 76 | public Job userJob() throws Exception { 77 | return this.jobBuilderFactory.get(JOB_NAME) 78 | .incrementer(new RunIdIncrementer()) 79 | .start(this.saveUserStep()) 80 | .next(this.userLevelUpManagerStep()) 81 | .listener(new LevelUpJobExecutionListener(userRepository)) 82 | .next(new JobParametersDecide("date")) 83 | .on(JobParametersDecide.CONTINUE.getName()) 84 | .to(this.orderStatisticsStep(null)) 85 | .build() 86 | .build(); 87 | } 88 | 89 | @Bean(JOB_NAME + "_orderStatisticsStep") 90 | @JobScope 91 | public Step orderStatisticsStep(@Value("#{jobParameters[date]}") String date) throws Exception { 92 | return this.stepBuilderFactory.get(JOB_NAME + "_orderStatisticsStep") 93 | .chunk(CHUNK) 94 | .reader(orderStatisticsItemReader(date)) 95 | .writer(orderStatisticsItemWriter(date)) 96 | .build(); 97 | } 98 | 99 | private ItemWriter orderStatisticsItemWriter(String date) throws Exception { 100 | YearMonth yearMonth = YearMonth.parse(date); 101 | String fileName = yearMonth.getYear() + "년_" + yearMonth.getMonthValue() + "월_일별_주문_금액.csv"; 102 | 103 | BeanWrapperFieldExtractor fieldExtractor = new BeanWrapperFieldExtractor<>(); 104 | fieldExtractor.setNames(new String[]{"amount", "date"}); 105 | 106 | DelimitedLineAggregator lineAggregator = new DelimitedLineAggregator<>(); 107 | lineAggregator.setDelimiter(","); 108 | lineAggregator.setFieldExtractor(fieldExtractor); 109 | 110 | FlatFileItemWriter itemWriter = new FlatFileItemWriterBuilder() 111 | .resource(new FileSystemResource("output/" + fileName)) 112 | .lineAggregator(lineAggregator) 113 | .name(JOB_NAME + "_orderStatisticsItemWriter") 114 | .encoding("UTF-8") 115 | .headerCallback(writer -> writer.write("total_amoun,date")) 116 | .build(); 117 | itemWriter.afterPropertiesSet(); 118 | 119 | return itemWriter; 120 | } 121 | 122 | private ItemReader orderStatisticsItemReader(String date) throws Exception { 123 | YearMonth yearMonth = YearMonth.parse(date); 124 | 125 | Map parameters = new HashMap<>(); 126 | parameters.put("startDate", yearMonth.atDay(1)); 127 | parameters.put("endDate", yearMonth.atEndOfMonth()); 128 | 129 | Map sortKey = new HashMap<>(); 130 | sortKey.put("created_date", Order.ASCENDING); 131 | 132 | JdbcPagingItemReader itemReader = new JdbcPagingItemReaderBuilder() 133 | .dataSource(this.dataSource) 134 | .rowMapper((resultSet, i) -> OrderStatistics.builder() 135 | .amount(resultSet.getString(1)) 136 | .date(LocalDate.parse(resultSet.getString(2), DateTimeFormatter.ISO_DATE)) 137 | .build()) 138 | .pageSize(CHUNK) 139 | .name(JOB_NAME + "_orderStatisticsItemReader") 140 | .selectClause("sum(amount), created_date") 141 | .fromClause("orders") 142 | .whereClause("created_date >= :startDate and created_date <= :endDate") 143 | .groupClause("created_date") 144 | .parameterValues(parameters) 145 | .sortKeys(sortKey) 146 | .build(); 147 | itemReader.afterPropertiesSet(); 148 | return itemReader; 149 | } 150 | 151 | @Bean(JOB_NAME + "_saveUserStep") 152 | public Step saveUserStep() { 153 | return this.stepBuilderFactory.get(JOB_NAME + "_saveUserStep") 154 | .tasklet(new SaveUserTasklet(userRepository)) 155 | .build(); 156 | } 157 | 158 | @Bean(JOB_NAME + "_userLevelUpStep") 159 | public Step userLevelUpStep() throws Exception { 160 | return this.stepBuilderFactory.get(JOB_NAME + "_userLevelUpStep") 161 | .>chunk(CHUNK) 162 | .reader(itemReader(null, null)) 163 | .processor(itemProcessor()) 164 | .writer(itemWriter()) 165 | .build(); 166 | } 167 | 168 | @Bean(JOB_NAME + "_userLevelUpStep.manager") 169 | public Step userLevelUpManagerStep() throws Exception { 170 | return this.stepBuilderFactory.get(JOB_NAME + "_userLevelUpStep.manager") 171 | .partitioner(JOB_NAME + "_userLevelUpStep", new UserLevelUpPartitioner(userRepository)) 172 | .step(userLevelUpStep()) 173 | .partitionHandler(taskExecutorPartitionHandler()) 174 | .build(); 175 | } 176 | 177 | @Bean(JOB_NAME + "_taskExecutorPartitionHandler") 178 | PartitionHandler taskExecutorPartitionHandler() throws Exception { 179 | TaskExecutorPartitionHandler handler = new TaskExecutorPartitionHandler(); 180 | handler.setStep(userLevelUpStep()); 181 | handler.setTaskExecutor(this.taskExecutor); 182 | handler.setGridSize(8); 183 | 184 | return handler; 185 | } 186 | 187 | private AsyncItemWriter itemWriter() { 188 | ItemWriter itemWriter = users -> users.forEach(x -> { 189 | x.levelUp(); 190 | userRepository.save(x); 191 | }); 192 | 193 | AsyncItemWriter asyncItemWriter = new AsyncItemWriter<>(); 194 | asyncItemWriter.setDelegate(itemWriter); 195 | 196 | return asyncItemWriter; 197 | } 198 | 199 | private AsyncItemProcessor itemProcessor() { 200 | ItemProcessor itemProcessor = user -> { 201 | if (user.availableLeveUp()) { 202 | return user; 203 | } 204 | 205 | return null; 206 | }; 207 | 208 | AsyncItemProcessor asyncItemProcessor = new AsyncItemProcessor<>(); 209 | asyncItemProcessor.setDelegate(itemProcessor); 210 | asyncItemProcessor.setTaskExecutor(this.taskExecutor); 211 | 212 | return asyncItemProcessor; 213 | } 214 | 215 | @Bean 216 | @StepScope 217 | JpaPagingItemReader itemReader(@Value("#{stepExecutionContext[minId]}") Long minId, 218 | @Value("#{stepExecutionContext[maxId]}") Long maxId) throws Exception { 219 | 220 | Map parameters = new HashMap<>(); 221 | parameters.put("minId", minId); 222 | parameters.put("maxId", maxId); 223 | 224 | JpaPagingItemReader itemReader = new JpaPagingItemReaderBuilder() 225 | .queryString("select u from User u where u.id between :minId and :maxId") 226 | .parameterValues(parameters) 227 | .entityManagerFactory(entityManagerFactory) 228 | .pageSize(CHUNK) 229 | .name(JOB_NAME + "_userItemReader") 230 | .build(); 231 | 232 | itemReader.afterPropertiesSet(); 233 | 234 | return itemReader; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/main/java/fastcampus/spring/batch/part6/UserLevelUpPartitioner.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part6; 2 | 3 | import fastcampus.spring.batch.part4.UserRepository; 4 | import org.springframework.batch.core.partition.support.Partitioner; 5 | import org.springframework.batch.item.ExecutionContext; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | public class UserLevelUpPartitioner implements Partitioner { 11 | private final UserRepository userRepository; 12 | 13 | public UserLevelUpPartitioner(UserRepository userRepository) { 14 | this.userRepository = userRepository; 15 | } 16 | 17 | @Override 18 | public Map partition(int gridSize) { 19 | long minId = userRepository.findMinId(); // 1 20 | long maxId = userRepository.findMaxId(); // 40,000 21 | 22 | long targetSize = (maxId - minId) / gridSize + 1; // 5000 23 | 24 | /** 25 | * partition0 : 1, 5000 26 | * partition1 : 5001, 10000 27 | * ... 28 | * partition7 : 35001, 40000 29 | */ 30 | Map result = new HashMap<>(); 31 | 32 | long number = 0; 33 | 34 | long start = minId; 35 | 36 | long end = start + targetSize - 1; 37 | 38 | while (start <= maxId) { 39 | ExecutionContext value = new ExecutionContext(); 40 | 41 | result.put("partition" + number, value); 42 | 43 | if (end >= maxId) { 44 | end = maxId; 45 | } 46 | 47 | value.putLong("minId", start); 48 | value.putLong("maxId", end); 49 | 50 | start += targetSize; 51 | end += targetSize; 52 | number++; 53 | } 54 | 55 | return result; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/resources/application-mysql.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | hikari: 4 | jdbc-url: jdbc:mysql://127.0.0.1:3306/spring_batch?characterEncoding=UTF-8&serverTimezone=UTC&rewriteBatchedStatements=true 5 | driver-class-name: com.mysql.cj.jdbc.Driver 6 | username: root 7 | password: q1w2e3r4 8 | jpa: 9 | hibernate: 10 | ddl-auto: update 11 | show-sql: true 12 | batch: 13 | initialize-schema: never 14 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | batch: 3 | job: 4 | names: ${job.name:NONE} 5 | initialize-schema: 6 | datasource: 7 | driver-class-name: org.h2.Driver 8 | data: classpath:person.sql 9 | -------------------------------------------------------------------------------- /src/main/resources/person.csv: -------------------------------------------------------------------------------- 1 | 이름,나이,거주지 2 | 이경원,32,인천 3 | 홍길동,30,서울 4 | 아무개,25,강원 5 | 이경원,32,인천 6 | 홍길동,30,서울 7 | 아무개,25,강원 8 | 이경원,32,인천 9 | 홍길동,30,서울 10 | 아무개,25,강원 11 | 이경원,32,인천 12 | 홍길동,30,서울 13 | 아무개,25,강원 14 | 이경원,32,인천 15 | 홍길동,30,서울 16 | 아무개,25,강원 17 | 이경원,32,인천 18 | 홍길동,30,서울 19 | 아무개,25,강원 20 | 이경원,32,인천 21 | 홍길동,30,서울 22 | 아무개,25,강원 23 | 이경원,32,인천 24 | 홍길동,30,서울 25 | 아무개,25,강원 26 | 이경원,32,인천 27 | 홍길동,30,서울 28 | 아무개,25,강원 29 | 이경원,32,인천 30 | 홍길동,30,서울 31 | 아무개,25,강원 32 | 이경원,32,인천 33 | 홍길동,30,서울 34 | 아무개,25,강원 35 | 이경원,32,인천 36 | 홍길동,30,서울 37 | 아무개,25,강원 38 | 이경원,32,인천 39 | 홍길동,30,서울 40 | 아무개,25,강원 41 | 이경원,32,인천 42 | 홍길동,30,서울 43 | 아무개,25,강원 44 | 이경원,32,인천 45 | 홍길동,30,서울 46 | 아무개,25,강원 47 | 이경원,32,인천 48 | 홍길동,30,서울 49 | 아무개,25,강원 50 | 이경원,32,인천 51 | 홍길동,30,서울 52 | 아무개,25,강원 53 | 이경원,32,인천 54 | 홍길동,30,서울 55 | 아무개,25,강원 56 | 이경원,32,인천 57 | 홍길동,30,서울 58 | 아무개,25,강원 59 | 이경원,32,인천 60 | 홍길동,30,서울 61 | 아무개,25,강원 62 | 이경원,32,인천 63 | 홍길동,30,서울 64 | 아무개,25,강원 65 | 이경원,32,인천 66 | 홍길동,30,서울 67 | 아무개,25,강원 68 | 이경원,32,인천 69 | 홍길동,30,서울 70 | 아무개,25,강원 71 | 이경원,32,인천 72 | 홍길동,30,서울 73 | 아무개,25,강원 74 | 이경원,32,인천 75 | 홍길동,30,서울 76 | 아무개,25,강원 77 | 이경원,32,인천 78 | 홍길동,30,서울 79 | 아무개,25,강원 80 | 이경원,32,인천 81 | 홍길동,30,서울 82 | 아무개,25,강원 83 | 이경원,32,인천 84 | 홍길동,30,서울 85 | 아무개,25,강원 86 | 이경원,32,인천 87 | 홍길동,30,서울 88 | 아무개,25,강원 89 | 이경원,32,인천 90 | 홍길동,30,서울 91 | 아무개,25,강원 92 | 이경원,32,인천 93 | 홍길동,30,서울 94 | 아무개,25,강원 95 | 이경원,32,인천 96 | 홍길동,30,서울 97 | 아무개,25,강원 98 | 이경원,32,인천 99 | 홍길동,30,서울 100 | 아무개,25,강원 101 | 이경원,32,인천 102 | ,30,서울 103 | ,25,강원 104 | ,32,인천 105 | -------------------------------------------------------------------------------- /src/main/resources/person.sql: -------------------------------------------------------------------------------- 1 | -- create table person ( 2 | -- id bigint primary key auto_increment, 3 | -- name varchar(255), 4 | -- age varchar(255), 5 | -- address varchar(255) 6 | -- ); 7 | 8 | insert into person(name, age, address) 9 | values('이경원','32','인천'); 10 | insert into person(name, age, address) 11 | values('홍길동','30','서울'); 12 | insert into person(name, age, address) 13 | values('아무개','25','강원'); 14 | -------------------------------------------------------------------------------- /src/main/resources/test.csv: -------------------------------------------------------------------------------- 1 | id,이름,나이,거주지 2 | 1,이경원,32,인천 3 | 2,홍길동,30,서울 4 | 3,아무개,25,강원 5 | -------------------------------------------------------------------------------- /src/test/java/fastcampus/spring/batch/SpringBatchExampleApplicationTests.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class SpringBatchExampleApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/fastcampus/spring/batch/TestConfiguration.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch; 2 | 3 | import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; 4 | import org.springframework.batch.test.JobLauncherTestUtils; 5 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | @EnableBatchProcessing 11 | @EnableAutoConfiguration 12 | public class TestConfiguration { 13 | 14 | @Bean 15 | public JobLauncherTestUtils jobLauncherTestUtils() { 16 | return new JobLauncherTestUtils(); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/fastcampus/spring/batch/part3/SavePersonConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part3; 2 | 3 | import fastcampus.spring.batch.TestConfiguration; 4 | import org.assertj.core.api.Assertions; 5 | import org.junit.After; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.springframework.batch.core.JobExecution; 9 | import org.springframework.batch.core.JobParameters; 10 | import org.springframework.batch.core.JobParametersBuilder; 11 | import org.springframework.batch.core.StepExecution; 12 | import org.springframework.batch.test.JobLauncherTestUtils; 13 | import org.springframework.batch.test.context.SpringBatchTest; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.test.context.ContextConfiguration; 16 | import org.springframework.test.context.junit4.SpringRunner; 17 | 18 | @SpringBatchTest 19 | @RunWith(SpringRunner.class) 20 | @ContextConfiguration(classes = {SavePersonConfiguration.class, TestConfiguration.class}) 21 | public class SavePersonConfigurationTest { 22 | 23 | @Autowired 24 | private JobLauncherTestUtils jobLauncherTestUtils; 25 | 26 | @Autowired 27 | private PersonRepository personRepository; 28 | 29 | @After 30 | public void tearDown() throws Exception { 31 | personRepository.deleteAll(); 32 | } 33 | 34 | @Test 35 | public void test_step() { 36 | JobExecution jobExecution = jobLauncherTestUtils.launchStep("savePersonStep"); 37 | 38 | Assertions.assertThat(jobExecution.getStepExecutions().stream() 39 | .mapToInt(StepExecution::getWriteCount) 40 | .sum()) 41 | .isEqualTo(personRepository.count()) 42 | .isEqualTo(3); 43 | } 44 | 45 | @Test 46 | public void test_allow_duplicate() throws Exception { 47 | // given 48 | JobParameters jobParameters = new JobParametersBuilder() 49 | .addString("allow_duplicate", "false") 50 | .toJobParameters(); 51 | 52 | // when 53 | JobExecution jobExecution = jobLauncherTestUtils.launchJob(jobParameters); 54 | 55 | // then 56 | Assertions.assertThat(jobExecution.getStepExecutions().stream() 57 | .mapToInt(StepExecution::getWriteCount) 58 | .sum()) 59 | .isEqualTo(personRepository.count()) 60 | .isEqualTo(3); 61 | } 62 | 63 | @Test 64 | public void test_not_allow_duplicate() throws Exception { 65 | // given 66 | JobParameters jobParameters = new JobParametersBuilder() 67 | .addString("allow_duplicate", "true") 68 | .toJobParameters(); 69 | 70 | // when 71 | JobExecution jobExecution = jobLauncherTestUtils.launchJob(jobParameters); 72 | 73 | // then 74 | Assertions.assertThat(jobExecution.getStepExecutions().stream() 75 | .mapToInt(StepExecution::getWriteCount) 76 | .sum()) 77 | .isEqualTo(personRepository.count()) 78 | .isEqualTo(100); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/fastcampus/spring/batch/part4/ParallelUserConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package fastcampus.spring.batch.part4; 2 | 3 | import fastcampus.spring.batch.TestConfiguration; 4 | import org.assertj.core.api.Assertions; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.batch.core.JobExecution; 8 | import org.springframework.batch.core.StepExecution; 9 | import org.springframework.batch.test.JobLauncherTestUtils; 10 | import org.springframework.batch.test.context.SpringBatchTest; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.test.context.ContextConfiguration; 13 | import org.springframework.test.context.junit4.SpringRunner; 14 | 15 | import java.time.LocalDate; 16 | 17 | @SpringBatchTest 18 | @RunWith(SpringRunner.class) 19 | @ContextConfiguration(classes = {UserConfiguration.class, TestConfiguration.class}) 20 | public class ParallelUserConfigurationTest { 21 | 22 | @Autowired 23 | private JobLauncherTestUtils jobLauncherTestUtils; 24 | 25 | @Autowired 26 | private UserRepository userRepository; 27 | 28 | @Test 29 | public void test() throws Exception { 30 | JobExecution jobExecution = jobLauncherTestUtils.launchJob(); 31 | 32 | int size = userRepository.findAllByUpdatedDate(LocalDate.now()).size(); 33 | 34 | Assertions.assertThat(jobExecution.getStepExecutions().stream() 35 | .filter(x -> x.getStepName().equals("userLevelUpStep")) 36 | .mapToInt(StepExecution::getWriteCount) 37 | .sum()) 38 | .isEqualTo(size) 39 | .isEqualTo(300); 40 | 41 | Assertions.assertThat(userRepository.count()) 42 | .isEqualTo(400); 43 | } 44 | } 45 | --------------------------------------------------------------------------------