From f04d6df3248ee4fbf22d7835a7af98c4cdfb189e Mon Sep 17 00:00:00 2001 From: mindol1004 Date: Thu, 22 Aug 2024 16:01:29 +0900 Subject: [PATCH] first Commit!!! --- .vscode/launch.json | 21 ++ .vscode/settings.json | 51 ++++ batch-quartz/.gitignore | 33 +++ .../.mvn/wrapper/maven-wrapper.properties | 19 ++ batch-quartz/mvnw | 259 ++++++++++++++++++ batch-quartz/mvnw.cmd | 149 ++++++++++ batch-quartz/pom.xml | 132 +++++++++ .../com/spring/BatchQuartzApplication.java | 15 + .../com/spring/common/error/BizException.java | 9 + .../batch/entity/BatchJobExecution.java | 54 ++++ .../entity/BatchJobExecutionContext.java | 31 +++ .../batch/entity/BatchJobExecutionParams.java | 48 ++++ .../domain/batch/entity/BatchJobInstance.java | 32 +++ .../batch/entity/BatchStepExecution.java | 81 ++++++ .../entity/BatchStepExecutionContext.java | 31 +++ .../domain/email/entity/ComMailqueMst.java | 113 ++++++++ .../domain/email/entity/ComMailrstLog.java | 77 ++++++ .../com/spring/domain/post/entity/Post.java | 33 +++ .../post/repository/PostRepository.java | 12 + .../quartz/entity/QrtzBlobTriggers.java | 49 ++++ .../domain/quartz/entity/QrtzCalendars.java | 35 +++ .../quartz/entity/QrtzCronTriggers.java | 53 ++++ .../quartz/entity/QrtzFiredTriggers.java | 65 +++++ .../domain/quartz/entity/QrtzJobDetails.java | 56 ++++ .../domain/quartz/entity/QrtzLocks.java | 32 +++ .../quartz/entity/QrtzPausedTriggerGrps.java | 32 +++ .../quartz/entity/QrtzSchedulerState.java | 38 +++ .../quartz/entity/QrtzSimpleTriggers.java | 56 ++++ .../quartz/entity/QrtzSimpropTriggers.java | 81 ++++++ .../domain/quartz/entity/QrtzTriggers.java | 80 ++++++ .../domain/user/api/AuthController.java | 33 +++ .../spring/domain/user/dto/SignInRequest.java | 13 + .../spring/domain/user/entity/AppUser.java | 40 +++ .../domain/user/entity/AppUserRole.java | 34 +++ .../domain/user/entity/AppUserRoleMap.java | 48 ++++ .../user/repository/AppUserRepository.java | 13 + .../repository/AppUserRoleMapRepository.java | 10 + .../domain/user/service/AuthService.java | 23 ++ .../infra/batch/AbstractBatchConfig.java | 19 ++ .../infra/db/PrimaryDataSourceConfig.java | 35 +++ .../infra/db/SecondaryDataSourceConfig.java | 32 +++ .../infra/db/annotation/DatabaseSelector.java | 12 + .../infra/db/orm/jpa/PrimaryJpaConfig.java | 69 +++++ .../infra/db/orm/jpa/SecondaryJpaConfig.java | 65 +++++ .../orm/jpa/util/DatabaseSelectorFilter.java | 33 +++ .../orm/jpa/util/EntityScannerConfigurer.java | 36 +++ .../db/orm/mybatis/PrimaryMybatisConfig.java | 63 +++++ .../orm/mybatis/SecondaryMybatisConfig.java | 59 ++++ .../util/MybatisBeanNameGenerator.java | 41 +++ .../com/spring/infra/quartz/QuartzConfig.java | 80 ++++++ .../com/spring/infra/quartz/QuartzJob.java | 13 + .../infra/quartz/QuartzJobLauncher.java | 44 +++ .../infra/quartz/QuartzJobRegistrar.java | 80 ++++++ .../spring/infra/quartz/QuartzProperties.java | 87 ++++++ .../infra/security/config/SecurityConfig.java | 71 +++++ .../infra/security/domain/UserPrincipal.java | 63 +++++ .../filter/JwtAuthenticationFilter.java | 54 ++++ .../handler/JwtAccessDeniedHandler.java | 25 ++ .../handler/JwtAuthenticationEntryPoint.java | 25 ++ .../infra/security/jwt/JwtProperties.java | 23 ++ .../infra/security/jwt/JwtTokenGenerator.java | 60 ++++ .../infra/security/jwt/JwtTokenRule.java | 19 ++ .../infra/security/jwt/JwtTokenService.java | 106 +++++++ .../infra/security/jwt/JwtTokenStatus.java | 12 + .../infra/security/jwt/JwtTokenUtil.java | 61 +++++ .../service/UserPrincipalService.java | 29 ++ ...itional-spring-configuration-metadata.json | 147 ++++++++++ .../src/main/resources/application.yml | 85 ++++++ .../src/main/resources/batch-schema.sql | 78 ++++++ .../src/main/resources/quartz-schema.sql | 238 ++++++++++++++++ batch-quartz/src/main/resources/quartz.yml | 40 +++ .../BatchQuartzApplicationTests.java | 13 + 72 files changed, 3908 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 batch-quartz/.gitignore create mode 100644 batch-quartz/.mvn/wrapper/maven-wrapper.properties create mode 100644 batch-quartz/mvnw create mode 100644 batch-quartz/mvnw.cmd create mode 100644 batch-quartz/pom.xml create mode 100644 batch-quartz/src/main/java/com/spring/BatchQuartzApplication.java create mode 100644 batch-quartz/src/main/java/com/spring/common/error/BizException.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchJobExecution.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchJobExecutionContext.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchJobExecutionParams.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchJobInstance.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchStepExecution.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchStepExecutionContext.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/email/entity/ComMailqueMst.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/email/entity/ComMailrstLog.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/post/entity/Post.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/post/repository/PostRepository.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzBlobTriggers.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzCalendars.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzCronTriggers.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzFiredTriggers.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzJobDetails.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzLocks.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzPausedTriggerGrps.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzSchedulerState.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzSimpleTriggers.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzSimpropTriggers.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzTriggers.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/user/api/AuthController.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/user/dto/SignInRequest.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/user/entity/AppUser.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/user/entity/AppUserRole.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/user/entity/AppUserRoleMap.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/user/repository/AppUserRepository.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/user/repository/AppUserRoleMapRepository.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/user/service/AuthService.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchConfig.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/db/PrimaryDataSourceConfig.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/db/SecondaryDataSourceConfig.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/db/annotation/DatabaseSelector.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/PrimaryJpaConfig.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/SecondaryJpaConfig.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/util/DatabaseSelectorFilter.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/util/EntityScannerConfigurer.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/db/orm/mybatis/PrimaryMybatisConfig.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/db/orm/mybatis/SecondaryMybatisConfig.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/db/orm/mybatis/util/MybatisBeanNameGenerator.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/quartz/QuartzConfig.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJob.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobLauncher.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobRegistrar.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/quartz/QuartzProperties.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/security/config/SecurityConfig.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/security/domain/UserPrincipal.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/security/filter/JwtAuthenticationFilter.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/security/handler/JwtAccessDeniedHandler.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/security/handler/JwtAuthenticationEntryPoint.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtProperties.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenGenerator.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenRule.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenService.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenStatus.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenUtil.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/security/service/UserPrincipalService.java create mode 100644 batch-quartz/src/main/resources/META-INF/additional-spring-configuration-metadata.json create mode 100644 batch-quartz/src/main/resources/application.yml create mode 100644 batch-quartz/src/main/resources/batch-schema.sql create mode 100644 batch-quartz/src/main/resources/quartz-schema.sql create mode 100644 batch-quartz/src/main/resources/quartz.yml create mode 100644 batch-quartz/src/test/java/com/spring/batch_quartz/BatchQuartzApplicationTests.java diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..284d8d8 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + "configurations": [ + { + "type": "java", + "name": "BatchQuartzApplication", + "request": "launch", + "mainClass": "com.spring.BatchQuartzApplication", + "projectName": "batch-quartz" + }, + { + "type": "java", + "name": "Spring Boot-BatchQuartzApplication", + "request": "launch", + "cwd": "${workspaceFolder}", + "mainClass": "com.spring.batch_quartz.BatchQuartzApplication", + "projectName": "batch-quartz", + "args": "", + "envFile": "${workspaceFolder}/.env" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..95c738d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,51 @@ +{ + "java.compile.nullAnalysis.mode": "automatic", + "cSpell.words": [ + "attachfile", + "Autowire", + "cdate", + "checkin", + "classpath", + "Configurer", + "CTNOPS", + "dbcode", + "Embeddable", + "Grps", + "Hikari", + "ibatis", + "insertable", + "issecure", + "JDBC", + "jdbcjobstore", + "jsonwebtoken", + "Jwts", + "lastflg", + "Mailque", + "Mailrst", + "mailtypename", + "mindol", + "Mybatis", + "Mybtis", + "nxcus", + "Qrtz", + "RCODE", + "rmail", + "RMSG", + "RNAME", + "RPOS", + "rscode", + "rsdetmsg", + "rsmsg", + "rstime", + "sched", + "sdate", + "securetemplate", + "Servlet", + "Simprop", + "smail", + "sname", + "stime", + "subid" + ], + "java.debug.settings.onBuildFailureProceed": true +} \ No newline at end of file diff --git a/batch-quartz/.gitignore b/batch-quartz/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/batch-quartz/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/batch-quartz/.mvn/wrapper/maven-wrapper.properties b/batch-quartz/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8f96f52 --- /dev/null +++ b/batch-quartz/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/batch-quartz/mvnw b/batch-quartz/mvnw new file mode 100644 index 0000000..d7c358e --- /dev/null +++ b/batch-quartz/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/batch-quartz/mvnw.cmd b/batch-quartz/mvnw.cmd new file mode 100644 index 0000000..6f779cf --- /dev/null +++ b/batch-quartz/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/batch-quartz/pom.xml b/batch-quartz/pom.xml new file mode 100644 index 0000000..8354471 --- /dev/null +++ b/batch-quartz/pom.xml @@ -0,0 +1,132 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + + com.spring + batch-quartz + 0.0.1-SNAPSHOT + batch-quartz + Demo project for Spring Boot + + + + + + + + + + + + + + + 17 + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-batch + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 3.0.3 + + + org.springframework.boot + spring-boot-starter-quartz + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + com.h2database + h2 + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.batch + spring-batch-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + diff --git a/batch-quartz/src/main/java/com/spring/BatchQuartzApplication.java b/batch-quartz/src/main/java/com/spring/BatchQuartzApplication.java new file mode 100644 index 0000000..14fe56c --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/BatchQuartzApplication.java @@ -0,0 +1,15 @@ +package com.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; + +@SpringBootApplication +@ConfigurationPropertiesScan +public class BatchQuartzApplication { + + public static void main(String[] args) { + SpringApplication.run(BatchQuartzApplication.class, args); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/common/error/BizException.java b/batch-quartz/src/main/java/com/spring/common/error/BizException.java new file mode 100644 index 0000000..be89fd8 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/common/error/BizException.java @@ -0,0 +1,9 @@ +package com.spring.common.error; + +public class BizException extends RuntimeException { + + public BizException(String message) { + super(message); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchJobExecution.java b/batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchJobExecution.java new file mode 100644 index 0000000..2b7b368 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchJobExecution.java @@ -0,0 +1,54 @@ +package com.spring.domain.batch.entity; + +import java.sql.Timestamp; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; + +@Entity +@Table(name = "BATCH_JOB_EXECUTION") +@Getter +public class BatchJobExecution { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "JOB_EXECUTION_ID") + private Long jobExecutionId; + + @Column(name = "VERSION") + private Long version; + + @ManyToOne + @JoinColumn(name = "JOB_INSTANCE_ID", nullable = false, foreignKey = @ForeignKey(name = "JOB_INST_EXEC_FK")) + private BatchJobInstance batchJobInstance; + + @Column(name = "CREATE_TIME", nullable = false) + private Timestamp createTime; + + @Column(name = "START_TIME") + private Timestamp startTime; + + @Column(name = "END_TIME") + private Timestamp endTime; + + @Column(name = "STATUS", length = 10) + private String status; + + @Column(name = "EXIT_CODE", length = 2500) + private String exitCode; + + @Column(name = "EXIT_MESSAGE", length = 2500) + private String exitMessage; + + @Column(name = "LAST_UPDATED") + private Timestamp lastUpdated; + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchJobExecutionContext.java b/batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchJobExecutionContext.java new file mode 100644 index 0000000..f52e2ab --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchJobExecutionContext.java @@ -0,0 +1,31 @@ +package com.spring.domain.batch.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.Getter; + +@Entity +@Table(name = "BATCH_JOB_EXECUTION_CONTEXT") +@Getter +public class BatchJobExecutionContext { + + @Id + @Column(name = "JOB_EXECUTION_ID") + private Long jobExecutionId; + + @Column(name = "SHORT_CONTEXT", length = 2500, nullable = false) + private String shortContext; + + @Column(name = "SERIALIZED_CONTEXT") + private String serializedContext; + + @OneToOne + @JoinColumn(name = "JOB_EXECUTION_ID", insertable = false, updatable = false, foreignKey = @ForeignKey(name = "JOB_EXEC_CTX_FK")) + private BatchJobExecution batchJobExecution; + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchJobExecutionParams.java b/batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchJobExecutionParams.java new file mode 100644 index 0000000..96be124 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchJobExecutionParams.java @@ -0,0 +1,48 @@ +package com.spring.domain.batch.entity; + +import java.io.Serializable; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Entity +@Table(name = "BATCH_JOB_EXECUTION_PARAMS") +@Getter +public class BatchJobExecutionParams { + + @EmbeddedId + private BatchJobExecutionParamsId id; + + @ManyToOne + @JoinColumn(name = "JOB_EXECUTION_ID", insertable = false, updatable = false, foreignKey = @ForeignKey(name = "JOB_EXEC_PARAMS_FK")) + private BatchJobExecution jobExecution; + + @Column(name = "PARAMETER_TYPE", nullable = false) + private String parameterType; + + @Column(name = "PARAMETER_VALUE", length = 2500) + private String parameterValue; + + @Column(name = "IDENTIFYING", nullable = false) + private Character identifying; + + @Embeddable + @Getter + @EqualsAndHashCode + public class BatchJobExecutionParamsId implements Serializable { + @Column(name = "JOB_EXECUTION_ID") + private Long jobExecutionId; + + @Column(name = "PARAMETER_NAME") + private String parameterName; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchJobInstance.java b/batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchJobInstance.java new file mode 100644 index 0000000..46e77d2 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchJobInstance.java @@ -0,0 +1,32 @@ +package com.spring.domain.batch.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.Getter; + +@Entity +@Table(name = "BATCH_JOB_INSTANCE", + uniqueConstraints = @UniqueConstraint(name = "JOB_INST_UN", columnNames = {"JOB_NAME", "JOB_KEY"})) +@Getter +public class BatchJobInstance { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "JOB_INSTANCE_ID") + private Long jobInstanceId; + + @Column(name = "VERSION") + private Long version; + + @Column(name = "JOB_NAME", length = 100, nullable = false) + private String jobName; + + @Column(name = "JOB_KEY", length = 32, nullable = false) + private String jobKey; + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchStepExecution.java b/batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchStepExecution.java new file mode 100644 index 0000000..b849301 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchStepExecution.java @@ -0,0 +1,81 @@ +package com.spring.domain.batch.entity; + +import java.sql.Timestamp; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; + +@Entity +@Table(name = "BATCH_STEP_EXECUTION") +@Getter +public class BatchStepExecution { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "STEP_EXECUTION_ID") + private Long stepExecutionId; + + @Column(name = "VERSION", nullable = false) + private Long version; + + @Column(name = "STEP_NAME", length = 100, nullable = false) + private String stepName; + + @ManyToOne + @JoinColumn(name = "JOB_EXECUTION_ID", nullable = false, foreignKey = @ForeignKey(name = "JOB_EXEC_STEP_FK")) + private BatchJobExecution batchJobExecution; + + @Column(name = "CREATE_TIME", nullable = false) + private Timestamp createTime; + + @Column(name = "START_TIME") + private Timestamp startTime; + + @Column(name = "END_TIME") + private Timestamp endTime; + + @Column(name = "STATUS", length = 10) + private String status; + + @Column(name = "COMMIT_COUNT") + private Long commitCount; + + @Column(name = "READ_COUNT") + private Long readCount; + + @Column(name = "FILTER_COUNT") + private Long filterCount; + + @Column(name = "WRITE_COUNT") + private Long writeCount; + + @Column(name = "READ_SKIP_COUNT") + private Long readSkipCount; + + @Column(name = "WRITE_SKIP_COUNT") + private Long writeSkipCount; + + @Column(name = "PROCESS_SKIP_COUNT") + private Long processSkipCount; + + @Column(name = "ROLLBACK_COUNT") + private Long rollbackCount; + + @Column(name = "EXIT_CODE", length = 2500) + private String exitCode; + + @Column(name = "EXIT_MESSAGE", length = 2500) + private String exitMessage; + + @Column(name = "LAST_UPDATED") + private Timestamp lastUpdated; + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchStepExecutionContext.java b/batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchStepExecutionContext.java new file mode 100644 index 0000000..b5393a1 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/batch/entity/BatchStepExecutionContext.java @@ -0,0 +1,31 @@ +package com.spring.domain.batch.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.Getter; + +@Entity +@Table(name = "BATCH_STEP_EXECUTION_CONTEXT") +@Getter +public class BatchStepExecutionContext { + + @Id + @Column(name = "STEP_EXECUTION_ID") + private Long stepExecutionId; + + @Column(name = "SHORT_CONTEXT", length = 2500, nullable = false) + private String shortContext; + + @Column(name = "SERIALIZED_CONTEXT") + private String serializedContext; + + @OneToOne + @JoinColumn(name = "STEP_EXECUTION_ID", insertable = false, updatable = false, foreignKey = @ForeignKey(name = "STEP_EXEC_CTX_FK")) + private BatchStepExecution batchStepExecution; + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/email/entity/ComMailqueMst.java b/batch-quartz/src/main/java/com/spring/domain/email/entity/ComMailqueMst.java new file mode 100644 index 0000000..d313b4a --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/email/entity/ComMailqueMst.java @@ -0,0 +1,113 @@ +package com.spring.domain.email.entity; + +import java.io.Serializable; +import java.time.LocalDate; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.Lob; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@SequenceGenerator( + name = "COM_MAILQUE_MST_SEQ", + sequenceName = "COM_MAILQUE_MST_SEQ", + initialValue = 1, + allocationSize = 1 +) +@Entity +@Table(name = "COM_MAILQUE_MST") +@IdClass(com.spring.domain.email.entity.ComMailqueMst.ComMailqueMstId.class) +public class ComMailqueMst { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "COM_MAILQUE_MST_SEQ") + @Column(name = "MID", nullable = false) + private Long mid; + + @Id + @Column(name = "SUBID", nullable = false) + private Long subid; + + @Column(name = "TID", nullable = false, length = 10) + private String tid; + + @Column(name = "MAILTYPENAME", length = 25) + private String mailtypename; + + @Column(name = "SNAME", nullable = false, length = 50) + private String sname; + + @Column(name = "SMAIL", nullable = false, length = 100) + private String smail; + + @Column(name = "SID", length = 50) + private String sid; + + @Column(name = "RPOS", nullable = false, length = 1) + private Character rpos; + + @Column(name = "QUERY", length = 2000) + private String query; + + @Column(name = "CTNOPS", length = 1) + private String ctnops; + + @Column(name = "SUBJECT", length = 100) + private String subject; + + @Lob + @Column(name = "CONTENTS") + private String contents; + + @Column(name = "CDATE") + private LocalDate cdate; + + @Column(name = "SDATE") + private LocalDate sdate; + + @Column(name = "STATUS", length = 1) + private String status; + + @Column(name = "DBCODE", length = 10) + private String dbcode; + + @Column(name = "CHARSET") + private Integer charset; + + @Column(name = "ISSECURE", length = 1) + private String issecure; + + @Column(name = "SECURETEMPLATE", length = 50) + private String securetemplate; + + @Column(name = "ATTACHFILE01", length = 255) + private String attachfile01; + + @Column(name = "ATTACHFILE02", length = 255) + private String attachfile02; + + @Column(name = "ATTACHFILE03", length = 255) + private String attachfile03; + + @Column(name = "ATTACHFILE04", length = 255) + private String attachfile04; + + @Column(name = "ATTACHFILE05", length = 255) + private String attachfile05; + + @Getter + @EqualsAndHashCode + public static class ComMailqueMstId implements Serializable { + private Long mid; + private Long subid; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/email/entity/ComMailrstLog.java b/batch-quartz/src/main/java/com/spring/domain/email/entity/ComMailrstLog.java new file mode 100644 index 0000000..832f3c7 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/email/entity/ComMailrstLog.java @@ -0,0 +1,77 @@ +package com.spring.domain.email.entity; + +import java.io.Serializable; +import java.time.LocalDate; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@Entity +@Table(name = "COM_MAILRST_LOG") +public class ComMailrstLog { + + @EmbeddedId + private ComMailrstLogId id; + + @Column(name = "TID", nullable = false, length = 20) + private String tid; + + @Column(name = "RNAME", length = 50) + private String rname; + + @Column(name = "SID", length = 50) + private String sid; + + @Column(name = "SNAME", nullable = false, length = 50) + private String sname; + + @Column(name = "SMAIL", nullable = false, length = 100) + private String smail; + + @Column(name = "RCODE", nullable = false, length = 2) + private Character rcode; + + @Column(name = "RMSG", length = 100) + private String rmsg; + + @Column(name = "STIME", nullable = false) + private LocalDate stime; + + @Column(name = "LASTFLG", length = 2) + private Character lastflg; + + @Column(name = "RSTIME") + private LocalDate rstime; + + @Column(name = "RSCODE", length = 2) + private Character rscode; + + @Column(name = "RSMSG", length = 100) + private String rsmsg; + + @Column(name = "RSDETMSG", length = 2000) + private String rsdetmsg; + + @Embeddable + @EqualsAndHashCode + @Getter + @Builder + public static class ComMailrstLogId implements Serializable { + @Column(name = "MID", nullable = false) + private Long mid; + + @Column(name = "SUBID", nullable = false) + private Long subid; + + @Column(name = "RMAIL", nullable = false, length = 100) + private String rmail; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/post/entity/Post.java b/batch-quartz/src/main/java/com/spring/domain/post/entity/Post.java new file mode 100644 index 0000000..d549f3f --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/post/entity/Post.java @@ -0,0 +1,33 @@ +package com.spring.domain.post.entity; + +import com.spring.infra.db.SecondaryDataSourceConfig; +import com.spring.infra.db.annotation.DatabaseSelector; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@DatabaseSelector(SecondaryDataSourceConfig.DATABASE) +@Entity +@Table(name = "APP_POST") +@Getter +@RequiredArgsConstructor +public class Post { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "POST_ID", nullable = false) + private final Long postId; + + @Column(name = "TITLE", nullable = false, length = 100) + private final String title; + + @Column(name = "CONTENT", nullable = false, length = 2000) + private final String content; + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/post/repository/PostRepository.java b/batch-quartz/src/main/java/com/spring/domain/post/repository/PostRepository.java new file mode 100644 index 0000000..ff66d2f --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/post/repository/PostRepository.java @@ -0,0 +1,12 @@ +package com.spring.domain.post.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.spring.domain.post.entity.Post; +import com.spring.infra.db.SecondaryDataSourceConfig; +import com.spring.infra.db.annotation.DatabaseSelector; + +@DatabaseSelector(SecondaryDataSourceConfig.DATABASE) +public interface PostRepository extends JpaRepository { + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzBlobTriggers.java b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzBlobTriggers.java new file mode 100644 index 0000000..7c0fbde --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzBlobTriggers.java @@ -0,0 +1,49 @@ +package com.spring.domain.quartz.entity; + +import java.io.Serializable; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinColumns; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Entity +@Table(name = "QRTZ_BLOB_TRIGGERS") +@Getter +public class QrtzBlobTriggers { + + @EmbeddedId + private QrtzBlobTriggersId id; + + @Column(name = "BLOB_DATA") + private byte[] blobData; + + @ManyToOne + @JoinColumns({ + @JoinColumn(name = "SCHED_NAME", referencedColumnName = "SCHED_NAME", insertable = false, updatable = false), + @JoinColumn(name = "TRIGGER_NAME", referencedColumnName = "TRIGGER_NAME", insertable = false, updatable = false), + @JoinColumn(name = "TRIGGER_GROUP", referencedColumnName = "TRIGGER_GROUP", insertable = false, updatable = false) + }) + private QrtzTriggers qrtzTriggers; + + @Embeddable + @Getter + @EqualsAndHashCode + public static class QrtzBlobTriggersId implements Serializable { + @Column(name = "SCHED_NAME", length = 120, nullable = false) + private String schedName; + + @Column(name = "TRIGGER_NAME", length = 200, nullable = false) + private String triggerName; + + @Column(name = "TRIGGER_GROUP", length = 200, nullable = false) + private String triggerGroup; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzCalendars.java b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzCalendars.java new file mode 100644 index 0000000..a68773a --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzCalendars.java @@ -0,0 +1,35 @@ +package com.spring.domain.quartz.entity; + +import java.io.Serializable; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Entity +@Table(name = "QRTZ_CALENDARS") +@Getter +public class QrtzCalendars { + + @EmbeddedId + private QrtzCalendarsId id; + + @Column(name = "CALENDAR", nullable = false) + private byte[] calendar; + + @Embeddable + @Getter + @EqualsAndHashCode + public static class QrtzCalendarsId implements Serializable { + @Column(name = "SCHED_NAME", length = 120, nullable = false) + private String schedName; + + @Column(name = "CALENDAR_NAME", length = 200, nullable = false) + private String calendarName; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzCronTriggers.java b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzCronTriggers.java new file mode 100644 index 0000000..ecb2283 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzCronTriggers.java @@ -0,0 +1,53 @@ +package com.spring.domain.quartz.entity; + +import java.io.Serializable; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinColumns; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Entity +@Table(name = "QRTZ_CRON_TRIGGERS") +@Getter +public class QrtzCronTriggers { + + @EmbeddedId + private QrtzCronTriggersId id; + + @Column(name = "CRON_EXPRESSION", length = 120, nullable = false) + private String cronExpression; + + @Column(name = "TIME_ZONE_ID", length = 80) + private String timeZoneId; + + @ManyToOne + @JoinColumns(value = { + @JoinColumn(name = "SCHED_NAME", referencedColumnName = "SCHED_NAME", insertable = false, updatable = false), + @JoinColumn(name = "TRIGGER_NAME", referencedColumnName = "TRIGGER_NAME", insertable = false, updatable = false), + @JoinColumn(name = "TRIGGER_GROUP", referencedColumnName = "TRIGGER_GROUP", insertable = false, updatable = false) + }, foreignKey = @ForeignKey(name = "FK_QRTZ_CRON_TRIGGERS_QRTZ_TRIGGERS")) + private QrtzTriggers qrtzTriggers; + + @Embeddable + @Getter + @EqualsAndHashCode + public static class QrtzCronTriggersId implements Serializable { + @Column(name = "SCHED_NAME", length = 120, nullable = false) + private String schedName; + + @Column(name = "TRIGGER_NAME", length = 200, nullable = false) + private String triggerName; + + @Column(name = "TRIGGER_GROUP", length = 200, nullable = false) + private String triggerGroup; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzFiredTriggers.java b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzFiredTriggers.java new file mode 100644 index 0000000..95d7306 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzFiredTriggers.java @@ -0,0 +1,65 @@ +package com.spring.domain.quartz.entity; + +import java.io.Serializable; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Entity +@Table(name = "QRTZ_FIRED_TRIGGERS") +@Getter +public class QrtzFiredTriggers { + + @EmbeddedId + private QrtzFiredTriggersId id; + + @Column(name = "TRIGGER_NAME", length = 200, nullable = false) + private String triggerName; + + @Column(name = "TRIGGER_GROUP", length = 200, nullable = false) + private String triggerGroup; + + @Column(name = "INSTANCE_NAME", length = 200, nullable = false) + private String instanceName; + + @Column(name = "FIRED_TIME", nullable = false) + private long firedTime; + + @Column(name = "SCHED_TIME", nullable = false) + private long schedTime; + + @Column(name = "PRIORITY", nullable = false) + private int priority; + + @Column(name = "STATE", length = 16, nullable = false) + private String state; + + @Column(name = "JOB_NAME", length = 200) + private String jobName; + + @Column(name = "JOB_GROUP", length = 200) + private String jobGroup; + + @Column(name = "IS_NONCONCURRENT") + private Boolean isNonConcurrent; + + @Column(name = "REQUESTS_RECOVERY") + private Boolean requestsRecovery; + + @Embeddable + @Getter + @EqualsAndHashCode + public static class QrtzFiredTriggersId implements Serializable { + @Column(name = "SCHED_NAME", length = 120, nullable = false) + private String schedName; + + @Column(name = "ENTRY_ID", length = 95, nullable = false) + private String entryId; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzJobDetails.java b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzJobDetails.java new file mode 100644 index 0000000..f0efbc4 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzJobDetails.java @@ -0,0 +1,56 @@ +package com.spring.domain.quartz.entity; + +import java.io.Serializable; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Entity +@Table(name = "QRTZ_JOB_DETAILS") +@Getter +public class QrtzJobDetails { + + @EmbeddedId + private QrtzJobDetailsId id; + + @Column(name = "DESCRIPTION", length = 250) + private String description; + + @Column(name = "JOB_CLASS_NAME", length = 250, nullable = false) + private String jobClassName; + + @Column(name = "IS_DURABLE", nullable = false) + private boolean isDurable; + + @Column(name = "IS_NONCONCURRENT", nullable = false) + private boolean isNonConcurrent; + + @Column(name = "IS_UPDATE_DATA", nullable = false) + private boolean isUpdateData; + + @Column(name = "REQUESTS_RECOVERY", nullable = false) + private boolean requestsRecovery; + + @Column(name = "JOB_DATA") + private byte[] jobData; + + @Embeddable + @Getter + @EqualsAndHashCode + public static class QrtzJobDetailsId implements Serializable { + @Column(name = "SCHED_NAME", length = 120, nullable = false) + private String schedName; + + @Column(name = "JOB_NAME", length = 200, nullable = false) + private String jobName; + + @Column(name = "JOB_GROUP", length = 200, nullable = false) + private String jobGroup; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzLocks.java b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzLocks.java new file mode 100644 index 0000000..184646f --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzLocks.java @@ -0,0 +1,32 @@ +package com.spring.domain.quartz.entity; + +import java.io.Serializable; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Entity +@Table(name = "QRTZ_LOCKS") +@Getter +public class QrtzLocks { + + @EmbeddedId + private QrtzLocksId id; + + @Embeddable + @Getter + @EqualsAndHashCode + public static class QrtzLocksId implements Serializable { + @Column(name = "SCHED_NAME", length = 120, nullable = false) + private String schedName; + + @Column(name = "LOCK_NAME", length = 40, nullable = false) + private String lockName; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzPausedTriggerGrps.java b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzPausedTriggerGrps.java new file mode 100644 index 0000000..f7189a9 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzPausedTriggerGrps.java @@ -0,0 +1,32 @@ +package com.spring.domain.quartz.entity; + +import java.io.Serializable; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Entity +@Table(name = "QRTZ_PAUSED_TRIGGER_GRPS") +@Getter +public class QrtzPausedTriggerGrps { + + @EmbeddedId + private QrtzPausedTriggerGrpsId id; + + @Embeddable + @Getter + @EqualsAndHashCode + public static class QrtzPausedTriggerGrpsId implements Serializable { + @Column(name = "SCHED_NAME", length = 120, nullable = false) + private String schedName; + + @Column(name = "TRIGGER_GROUP", length = 200, nullable = false) + private String triggerGroup; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzSchedulerState.java b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzSchedulerState.java new file mode 100644 index 0000000..98a8769 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzSchedulerState.java @@ -0,0 +1,38 @@ +package com.spring.domain.quartz.entity; + +import java.io.Serializable; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Entity +@Table(name = "QRTZ_SCHEDULER_STATE") +@Getter +public class QrtzSchedulerState { + + @EmbeddedId + private QrtzSchedulerStateId id; + + @Column(name = "LAST_CHECKIN_TIME", nullable = false) + private long lastCheckinTime; + + @Column(name = "CHECKIN_INTERVAL", nullable = false) + private long checkinInterval; + + @Embeddable + @Getter + @EqualsAndHashCode + public static class QrtzSchedulerStateId implements Serializable { + @Column(name = "SCHED_NAME", length = 120, nullable = false) + private String schedName; + + @Column(name = "INSTANCE_NAME", length = 200, nullable = false) + private String instanceName; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzSimpleTriggers.java b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzSimpleTriggers.java new file mode 100644 index 0000000..7fb92f3 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzSimpleTriggers.java @@ -0,0 +1,56 @@ +package com.spring.domain.quartz.entity; + +import java.io.Serializable; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinColumns; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Entity +@Table(name = "QRTZ_SIMPLE_TRIGGERS") +@Getter +public class QrtzSimpleTriggers { + + @EmbeddedId + private QrtzSimpleTriggersId id; + + @Column(name = "REPEAT_COUNT", nullable = false) + private long repeatCount; + + @Column(name = "REPEAT_INTERVAL", nullable = false) + private long repeatInterval; + + @Column(name = "TIMES_TRIGGERED", nullable = false) + private long timesTriggered; + + @ManyToOne + @JoinColumns(value = { + @JoinColumn(name = "SCHED_NAME", referencedColumnName = "SCHED_NAME", insertable = false, updatable = false), + @JoinColumn(name = "TRIGGER_NAME", referencedColumnName = "TRIGGER_NAME", insertable = false, updatable = false), + @JoinColumn(name = "TRIGGER_GROUP", referencedColumnName = "TRIGGER_GROUP", insertable = false, updatable = false) + }, foreignKey = @ForeignKey(name = "FK_QRTZ_SIMPLE_TRIGGERS_QRTZ_TRIGGERS")) + private QrtzTriggers qrtzTriggers; + + @Embeddable + @Getter + @EqualsAndHashCode + public static class QrtzSimpleTriggersId implements Serializable { + @Column(name = "SCHED_NAME", length = 120, nullable = false) + private String schedName; + + @Column(name = "TRIGGER_NAME", length = 200, nullable = false) + private String triggerName; + + @Column(name = "TRIGGER_GROUP", length = 200, nullable = false) + private String triggerGroup; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzSimpropTriggers.java b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzSimpropTriggers.java new file mode 100644 index 0000000..62f5ac8 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzSimpropTriggers.java @@ -0,0 +1,81 @@ +package com.spring.domain.quartz.entity; + +import java.io.Serializable; +import java.math.BigDecimal; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinColumns; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Entity +@Table(name = "QRTZ_SIMPROP_TRIGGERS") +@Getter +public class QrtzSimpropTriggers { + + @EmbeddedId + private QrtzSimpropTriggersId id; + + @Column(name = "STR_PROP_1", length = 512) + private String strProp1; + + @Column(name = "STR_PROP_2", length = 512) + private String strProp2; + + @Column(name = "STR_PROP_3", length = 512) + private String strProp3; + + @Column(name = "INT_PROP_1") + private Integer intProp1; + + @Column(name = "INT_PROP_2") + private Integer intProp2; + + @Column(name = "LONG_PROP_1") + private Long longProp1; + + @Column(name = "LONG_PROP_2") + private Long longProp2; + + @Column(name = "DEC_PROP_1", precision = 13, scale = 4) + private BigDecimal decProp1; + + @Column(name = "DEC_PROP_2", precision = 13, scale = 4) + private BigDecimal decProp2; + + @Column(name = "BOOL_PROP_1") + private Boolean boolProp1; + + @Column(name = "BOOL_PROP_2") + private Boolean boolProp2; + + @ManyToOne + @JoinColumns(value = { + @JoinColumn(name = "SCHED_NAME", referencedColumnName = "SCHED_NAME", insertable = false, updatable = false), + @JoinColumn(name = "TRIGGER_NAME", referencedColumnName = "TRIGGER_NAME", insertable = false, updatable = false), + @JoinColumn(name = "TRIGGER_GROUP", referencedColumnName = "TRIGGER_GROUP", insertable = false, updatable = false) + }, foreignKey = @ForeignKey(name = "FK_QRTZ_SIMPROP_TRIGGERS_QRTZ_TRIGGERS")) + private QrtzTriggers qrtzTriggers; + + @Embeddable + @Getter + @EqualsAndHashCode + public static class QrtzSimpropTriggersId implements Serializable { + @Column(name = "SCHED_NAME", length = 120, nullable = false) + private String schedName; + + @Column(name = "TRIGGER_NAME", length = 200, nullable = false) + private String triggerName; + + @Column(name = "TRIGGER_GROUP", length = 200, nullable = false) + private String triggerGroup; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzTriggers.java b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzTriggers.java new file mode 100644 index 0000000..8bcc98c --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/quartz/entity/QrtzTriggers.java @@ -0,0 +1,80 @@ +package com.spring.domain.quartz.entity; + +import java.io.Serializable; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinColumns; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Entity +@Table(name = "QRTZ_TRIGGERS") +@Getter +public class QrtzTriggers { + + @EmbeddedId + private QrtzTriggersId id; + + @Column(name = "DESCRIPTION", length = 250) + private String description; + + @Column(name = "NEXT_FIRE_TIME") + private Long nextFireTime; + + @Column(name = "PREV_FIRE_TIME") + private Long prevFireTime; + + @Column(name = "PRIORITY") + private Integer priority; + + @Column(name = "TRIGGER_STATE", length = 16, nullable = false) + private String triggerState; + + @Column(name = "TRIGGER_TYPE", length = 8, nullable = false) + private String triggerType; + + @Column(name = "START_TIME", nullable = false) + private Long startTime; + + @Column(name = "END_TIME") + private Long endTime; + + @Column(name = "CALENDAR_NAME", length = 200) + private String calendarName; + + @Column(name = "MISFIRE_INSTR") + private Short misfireInstr; + + @Column(name = "JOB_DATA") + private byte[] jobData; + + @ManyToOne + @JoinColumns(value = { + @JoinColumn(name = "SCHED_NAME", referencedColumnName = "SCHED_NAME", insertable = false, updatable = false), + @JoinColumn(name = "JOB_NAME", referencedColumnName = "JOB_NAME", insertable = false, updatable = false), + @JoinColumn(name = "JOB_GROUP", referencedColumnName = "JOB_GROUP", insertable = false, updatable = false) + }, foreignKey = @ForeignKey(name = "FK_QRTZ_TRIGGERS_QRTZ_JOB_DETAILS")) + private QrtzJobDetails qrtzJobDetails; + + @Embeddable + @Getter + @EqualsAndHashCode + public static class QrtzTriggersId implements Serializable { + @Column(name = "SCHED_NAME", length = 120, nullable = false) + private String schedName; + + @Column(name = "TRIGGER_NAME", length = 200, nullable = false) + private String triggerName; + + @Column(name = "TRIGGER_GROUP", length = 200, nullable = false) + private String triggerGroup; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/user/api/AuthController.java b/batch-quartz/src/main/java/com/spring/domain/user/api/AuthController.java new file mode 100644 index 0000000..395def2 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/user/api/AuthController.java @@ -0,0 +1,33 @@ +package com.spring.domain.user.api; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.spring.domain.user.dto.SignInRequest; +import com.spring.domain.user.service.AuthService; +import com.spring.infra.security.jwt.JwtTokenService; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class AuthController { + + private final AuthService authService; + private final JwtTokenService jwtTokenService; + + @PostMapping("/auth") + public ResponseEntity generateToken(HttpServletResponse response, @RequestBody SignInRequest request) { + Authentication auth = authService.getAuthentication(request.getUsername(), request.getPassword()); + jwtTokenService.generateAccessToken(response, auth); + jwtTokenService.generateRefreshToken(response, auth); + return ResponseEntity.ok().body(null); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/user/dto/SignInRequest.java b/batch-quartz/src/main/java/com/spring/domain/user/dto/SignInRequest.java new file mode 100644 index 0000000..5d1a106 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/user/dto/SignInRequest.java @@ -0,0 +1,13 @@ +package com.spring.domain.user.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class SignInRequest { + + private final String username; + private final String password; + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/user/entity/AppUser.java b/batch-quartz/src/main/java/com/spring/domain/user/entity/AppUser.java new file mode 100644 index 0000000..b9df1d7 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/user/entity/AppUser.java @@ -0,0 +1,40 @@ +package com.spring.domain.user.entity; + +import java.util.Set; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Entity +@Table(name = "APP_USER") +@Getter +@RequiredArgsConstructor +public final class AppUser { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "USER_ID", nullable = false) + private final Long userId; + + @Column(name = "LOGIN_ID", nullable = false, length = 50) + private final String loginId; + + @Column(name = "PASSWORD", nullable = false, length = 128) + private final String password; + + @Column(name = "USER_NAME", nullable = false, length = 50) + private final String userName; + + @OneToMany(mappedBy = "appUser", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private final Set appUserRoleMap; + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/user/entity/AppUserRole.java b/batch-quartz/src/main/java/com/spring/domain/user/entity/AppUserRole.java new file mode 100644 index 0000000..f2605ea --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/user/entity/AppUserRole.java @@ -0,0 +1,34 @@ +package com.spring.domain.user.entity; + +import java.util.Set; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Entity +@Table(name = "APP_USER_ROLE") +@Getter +@RequiredArgsConstructor +public final class AppUserRole { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ROLE_ID", nullable = false) + private final Long roleId; + + @Column(name = "ROLE_TYPE", nullable = false, length = 10) + private final String roleType; + + @OneToMany(mappedBy = "appUserRole", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private final Set appUserRoleMap; + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/user/entity/AppUserRoleMap.java b/batch-quartz/src/main/java/com/spring/domain/user/entity/AppUserRoleMap.java new file mode 100644 index 0000000..b6b69fd --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/user/entity/AppUserRoleMap.java @@ -0,0 +1,48 @@ +package com.spring.domain.user.entity; + +import java.io.Serializable; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MapsId; +import jakarta.persistence.Table; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Entity +@Table(name = "APP_USER_ROLE_MAP") +@Getter +@RequiredArgsConstructor +public final class AppUserRoleMap { + + @EmbeddedId + private final AppUserRoleMapId id; + + @ManyToOne + @MapsId("roleId") + @JoinColumn(name = "ROLE_ID") + private final AppUserRole appUserRole; + + @ManyToOne + @MapsId("userId") + @JoinColumn(name = "USER_ID") + private final AppUser appUser; + + @Embeddable + @EqualsAndHashCode + @Getter + @RequiredArgsConstructor + public static final class AppUserRoleMapId implements Serializable { + @Column(name = "ROLE_ID", nullable = false) + private final Long roleId; + + @Column(name = "USER_ID", nullable = false) + private final Long userId; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/user/repository/AppUserRepository.java b/batch-quartz/src/main/java/com/spring/domain/user/repository/AppUserRepository.java new file mode 100644 index 0000000..a9351a6 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/user/repository/AppUserRepository.java @@ -0,0 +1,13 @@ +package com.spring.domain.user.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.spring.domain.user.entity.AppUser; + +public interface AppUserRepository extends JpaRepository { + + Optional findByLoginId(String loginId); + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/user/repository/AppUserRoleMapRepository.java b/batch-quartz/src/main/java/com/spring/domain/user/repository/AppUserRoleMapRepository.java new file mode 100644 index 0000000..da91f34 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/user/repository/AppUserRoleMapRepository.java @@ -0,0 +1,10 @@ +package com.spring.domain.user.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.spring.domain.user.entity.AppUserRoleMap; +import com.spring.domain.user.entity.AppUserRoleMap.AppUserRoleMapId; + +public interface AppUserRoleMapRepository extends JpaRepository { + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/user/service/AuthService.java b/batch-quartz/src/main/java/com/spring/domain/user/service/AuthService.java new file mode 100644 index 0000000..bc24f70 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/user/service/AuthService.java @@ -0,0 +1,23 @@ +package com.spring.domain.user.service; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class AuthService { + + private final AuthenticationManagerBuilder authenticationManagerBuilder; + + @Transactional(readOnly = true) + public Authentication getAuthentication(String username, String password) { + var authenticationToken = new UsernamePasswordAuthenticationToken(username, password); + return authenticationManagerBuilder.getObject().authenticate(authenticationToken); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchConfig.java b/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchConfig.java new file mode 100644 index 0000000..ff42902 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchConfig.java @@ -0,0 +1,19 @@ +package com.spring.infra.batch; + +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; +import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableBatchProcessing +public abstract class AbstractBatchConfig { + + @Autowired + protected JobBuilderFactory jobBuilderFactory; + + @Autowired + protected StepBuilderFactory stepBuilderFactory; + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/db/PrimaryDataSourceConfig.java b/batch-quartz/src/main/java/com/spring/infra/db/PrimaryDataSourceConfig.java new file mode 100644 index 0000000..8b95889 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/db/PrimaryDataSourceConfig.java @@ -0,0 +1,35 @@ +package com.spring.infra.db; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import com.zaxxer.hikari.HikariDataSource; + +@Configuration +public class PrimaryDataSourceConfig { + + public static final String DATABASE = "primary"; + public static final String DATASOURCE = "primaryDataSource"; + private static final String DATASOURCE_PROPERTIES = "primaryDataSourceProperties"; + private static final String DATASOURCE_PROPERTIES_PREFIX = "spring.datasource.primary"; + + @Primary + @Bean(name = DATASOURCE_PROPERTIES) + @ConfigurationProperties(prefix = DATASOURCE_PROPERTIES_PREFIX) + DataSourceProperties dataSourceProperties() { + return new DataSourceProperties(); + } + + @Primary + @Bean(name = DATASOURCE) + DataSource dataSource(@Qualifier(DATASOURCE_PROPERTIES) DataSourceProperties properties) { + return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build(); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/db/SecondaryDataSourceConfig.java b/batch-quartz/src/main/java/com/spring/infra/db/SecondaryDataSourceConfig.java new file mode 100644 index 0000000..8da340c --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/db/SecondaryDataSourceConfig.java @@ -0,0 +1,32 @@ +package com.spring.infra.db; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.zaxxer.hikari.HikariDataSource; + +@Configuration +public class SecondaryDataSourceConfig { + + public static final String DATABASE = "secondary"; + public static final String DATASOURCE = "secondaryDataSource"; + private static final String DATASOURCE_PROPERTIES = "secondaryDataSourceProperties"; + private static final String DATASOURCE_PROPERTIES_PREFIX = "spring.datasource.secondary"; + + @Bean(name = DATASOURCE_PROPERTIES) + @ConfigurationProperties(prefix = DATASOURCE_PROPERTIES_PREFIX) + DataSourceProperties dataSourceProperties() { + return new DataSourceProperties(); + } + + @Bean(name = DATASOURCE) + DataSource dataSource(@Qualifier(DATASOURCE_PROPERTIES) DataSourceProperties properties) { + return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build(); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/db/annotation/DatabaseSelector.java b/batch-quartz/src/main/java/com/spring/infra/db/annotation/DatabaseSelector.java new file mode 100644 index 0000000..a28570f --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/db/annotation/DatabaseSelector.java @@ -0,0 +1,12 @@ +package com.spring.infra.db.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface DatabaseSelector { + String value(); +} diff --git a/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/PrimaryJpaConfig.java b/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/PrimaryJpaConfig.java new file mode 100644 index 0000000..0625912 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/PrimaryJpaConfig.java @@ -0,0 +1,69 @@ +package com.spring.infra.db.orm.jpa; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.Primary; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.transaction.PlatformTransactionManager; + +import com.spring.infra.db.PrimaryDataSourceConfig; +import com.spring.infra.db.orm.jpa.util.DatabaseSelectorFilter; +import com.spring.infra.db.orm.jpa.util.EntityScannerConfigurer; + +import jakarta.persistence.EntityManagerFactory; + +@Configuration +@EnableJpaRepositories( + basePackages = {PrimaryJpaConfig.BASE_PACKAGE}, + entityManagerFactoryRef = PrimaryJpaConfig.ENTITY_MANAGER_FACTORY, + transactionManagerRef = PrimaryJpaConfig.TRANSACTION_MANAGER, + includeFilters = @Filter(type = FilterType.CUSTOM, classes = DatabaseSelectorFilter.class) +) +public class PrimaryJpaConfig { + + public static final String TRANSACTION_MANAGER = "primaryTransactionManager"; + private static final String DATABASE_FILTER = "primaryDatabaseFilter"; + private static final String BASE_PACKAGE = "com.spring.domain"; + private static final String ENTITY_MANAGER_FACTORY = "primaryEntityManagerFactory"; + private static final String PERSISTENCE_UNIT = "primaryPersistenceUnit"; + + @Primary + @Bean(name = DATABASE_FILTER) + DatabaseSelectorFilter databaseSelectorFilter() { + var filter = new DatabaseSelectorFilter(); + filter.setDbName(PrimaryDataSourceConfig.DATABASE); + return filter; + } + + @Primary + @Bean(name = ENTITY_MANAGER_FACTORY) + LocalContainerEntityManagerFactoryBean entityManagerFactory( + EntityManagerFactoryBuilder builder, + @Qualifier(PrimaryDataSourceConfig.DATASOURCE) DataSource dataSource + ) { + var entities = EntityScannerConfigurer.scanEntities(BASE_PACKAGE, PrimaryDataSourceConfig.DATABASE); + return builder + .dataSource(dataSource) + .packages(entities.stream().map(BeanDefinition::getBeanClassName).toArray(String[]::new)) + .persistenceUnit(PERSISTENCE_UNIT) + .build(); + } + + @Primary + @Bean(TRANSACTION_MANAGER) + PlatformTransactionManager transactionManager( + @Qualifier(ENTITY_MANAGER_FACTORY) EntityManagerFactory entityManagerFactory + ) { + return new JpaTransactionManager(entityManagerFactory); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/SecondaryJpaConfig.java b/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/SecondaryJpaConfig.java new file mode 100644 index 0000000..5186d4c --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/SecondaryJpaConfig.java @@ -0,0 +1,65 @@ +package com.spring.infra.db.orm.jpa; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.transaction.PlatformTransactionManager; + +import com.spring.infra.db.SecondaryDataSourceConfig; +import com.spring.infra.db.orm.jpa.util.DatabaseSelectorFilter; +import com.spring.infra.db.orm.jpa.util.EntityScannerConfigurer; + +import jakarta.persistence.EntityManagerFactory; + +@Configuration +@EnableJpaRepositories( + basePackages = {SecondaryJpaConfig.BASE_PACKAGE}, + entityManagerFactoryRef = SecondaryJpaConfig.ENTITY_MANAGER_FACTORY, + transactionManagerRef = SecondaryJpaConfig.TRANSACTION_MANAGER, + includeFilters = @Filter(type = FilterType.CUSTOM, classes = DatabaseSelectorFilter.class) +) +public class SecondaryJpaConfig { + + public static final String TRANSACTION_MANAGER = "secondaryTransactionManager"; + private static final String DATABASE_FILTER = "secondaryDatabaseFilter"; + private static final String BASE_PACKAGE = "com.spring.domain"; + private static final String ENTITY_MANAGER_FACTORY = "secondaryEntityManagerFactory"; + private static final String PERSISTENCE_UNIT = "secondaryPersistenceUnit"; + + @Bean(name = DATABASE_FILTER) + DatabaseSelectorFilter databaseSelectorFilter() { + var filter = new DatabaseSelectorFilter(); + filter.setDbName(SecondaryDataSourceConfig.DATABASE); + return filter; + } + + @Bean(name = ENTITY_MANAGER_FACTORY) + LocalContainerEntityManagerFactoryBean entityManagerFactory( + EntityManagerFactoryBuilder builder, + @Qualifier(SecondaryDataSourceConfig.DATASOURCE) DataSource dataSource + ) { + var entities = EntityScannerConfigurer.scanEntities(BASE_PACKAGE, SecondaryDataSourceConfig.DATABASE); + return builder + .dataSource(dataSource) + .packages(entities.stream().map(BeanDefinition::getBeanClassName).toArray(String[]::new)) + .persistenceUnit(PERSISTENCE_UNIT) + .build(); + } + + @Bean(TRANSACTION_MANAGER) + PlatformTransactionManager transactionManager( + @Qualifier(ENTITY_MANAGER_FACTORY) EntityManagerFactory entityManagerFactory + ) { + return new JpaTransactionManager(entityManagerFactory); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/util/DatabaseSelectorFilter.java b/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/util/DatabaseSelectorFilter.java new file mode 100644 index 0000000..4514fc9 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/util/DatabaseSelectorFilter.java @@ -0,0 +1,33 @@ +package com.spring.infra.db.orm.jpa.util; + +import java.io.IOException; +import java.util.Optional; + +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.lang.NonNull; + +import com.spring.infra.db.annotation.DatabaseSelector; + +import lombok.Setter; + +public class DatabaseSelectorFilter implements TypeFilter { + + @Setter private String dbName; + + @Override + public boolean match(@NonNull MetadataReader metadataReader, @NonNull MetadataReaderFactory metadataReaderFactory) + throws IOException { + boolean hasAnnotation = metadataReader.getAnnotationMetadata().hasAnnotation(DatabaseSelector.class.getName()); + if (!hasAnnotation) { + return true; + } else { + return Optional.ofNullable(metadataReader.getAnnotationMetadata() + .getAnnotationAttributes(DatabaseSelector.class.getName())) + .map(attributes -> dbName.equals(attributes.get("value"))) + .orElse(false); + } + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/util/EntityScannerConfigurer.java b/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/util/EntityScannerConfigurer.java new file mode 100644 index 0000000..a53e0ca --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/util/EntityScannerConfigurer.java @@ -0,0 +1,36 @@ +package com.spring.infra.db.orm.jpa.util; + +import java.util.Optional; +import java.util.Set; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.lang.NonNull; + +import com.spring.infra.db.annotation.DatabaseSelector; + +public class EntityScannerConfigurer { + + public static Set scanEntities(String basePackage, String dbName) { + var scanner = new ClassPathScanningCandidateComponentProvider(false); + scanner.addIncludeFilter(new TypeFilter() { + @Override + public boolean match(@NonNull MetadataReader metadataReader, @NonNull MetadataReaderFactory metadataReaderFactory) { + boolean hasAnnotation = metadataReader.getAnnotationMetadata().hasAnnotation(DatabaseSelector.class.getName()); + if (!hasAnnotation) { + return true; + } else { + return Optional.ofNullable(metadataReader.getAnnotationMetadata() + .getAnnotationAttributes(DatabaseSelector.class.getName())) + .map(attributes -> dbName.equals(attributes.get("value"))) + .orElse(false); + } + } + }); + return scanner.findCandidateComponents(basePackage); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/db/orm/mybatis/PrimaryMybatisConfig.java b/batch-quartz/src/main/java/com/spring/infra/db/orm/mybatis/PrimaryMybatisConfig.java new file mode 100644 index 0000000..d402e3a --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/db/orm/mybatis/PrimaryMybatisConfig.java @@ -0,0 +1,63 @@ +package com.spring.infra.db.orm.mybatis; + +import javax.sql.DataSource; + +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.type.JdbcType; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.mybatis.spring.annotation.MapperScan; +import org.mybatis.spring.boot.autoconfigure.SpringBootVFS; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; + +import com.spring.infra.db.PrimaryDataSourceConfig; +import com.spring.infra.db.orm.mybatis.util.MybatisBeanNameGenerator; + +@Configuration +@MapperScan( + basePackages = PrimaryMybatisConfig.BASE_PACKAGE, + sqlSessionFactoryRef = PrimaryMybatisConfig.SESSION_FACTORY, + nameGenerator = MybatisBeanNameGenerator.class +) +public class PrimaryMybatisConfig { + + private static final String BEAN_NAME_GENERATOR = "primaryNameGenerator"; + private static final String BASE_PACKAGE = "com.spring.domain"; + private static final String ALIASES_PACKAGE = "com.spring.domain.*.*.dto"; + private static final String MAPPER_RESOURCES = "classpath:mapper/**/*.xml"; + private static final String MYBATIS_CONFIG = "primaryMybatisConfig"; + private static final String SESSION_FACTORY = "primarySqlSessionFactory"; + + @Primary + @Bean(name = BEAN_NAME_GENERATOR) + MybatisBeanNameGenerator mybatisBeanNameGenerator() { + return new MybatisBeanNameGenerator(PrimaryDataSourceConfig.DATABASE); + } + + @Primary + @Bean(name = MYBATIS_CONFIG) + org.apache.ibatis.session.Configuration mybatisConfiguration() { + var configuration = new org.apache.ibatis.session.Configuration(); + configuration.setMapUnderscoreToCamelCase(true); + configuration.setCallSettersOnNulls(true); + configuration.setReturnInstanceForEmptyRow(true); + configuration.setJdbcTypeForNull(JdbcType.NULL); + return configuration; + } + + @Primary + @Bean(name = SESSION_FACTORY) + SqlSessionFactory sqlSessionFactory(@Qualifier(PrimaryDataSourceConfig.DATASOURCE) DataSource dataSource) throws Exception { + var sessionFactory = new SqlSessionFactoryBean(); + sessionFactory.setConfiguration(mybatisConfiguration()); + sessionFactory.setDataSource(dataSource); + sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_RESOURCES)); + sessionFactory.setTypeAliasesPackage(ALIASES_PACKAGE); + sessionFactory.setVfs(SpringBootVFS.class); + return sessionFactory.getObject(); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/db/orm/mybatis/SecondaryMybatisConfig.java b/batch-quartz/src/main/java/com/spring/infra/db/orm/mybatis/SecondaryMybatisConfig.java new file mode 100644 index 0000000..4402545 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/db/orm/mybatis/SecondaryMybatisConfig.java @@ -0,0 +1,59 @@ +package com.spring.infra.db.orm.mybatis; + +import javax.sql.DataSource; + +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.type.JdbcType; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.mybatis.spring.annotation.MapperScan; +import org.mybatis.spring.boot.autoconfigure.SpringBootVFS; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; + +import com.spring.infra.db.SecondaryDataSourceConfig; +import com.spring.infra.db.orm.mybatis.util.MybatisBeanNameGenerator; + +@Configuration +@MapperScan( + basePackages = SecondaryMybatisConfig.BASE_PACKAGE, + sqlSessionFactoryRef = SecondaryMybatisConfig.SESSION_FACTORY, + nameGenerator = MybatisBeanNameGenerator.class +) +public class SecondaryMybatisConfig { + + private static final String BEAN_NAME_GENERATOR = "secondaryNameGenerator"; + private static final String BASE_PACKAGE = "com.spring.domain"; + private static final String ALIASES_PACKAGE = "com.spring.domain.*.*.dto"; + private static final String MAPPER_RESOURCES = "classpath:mapper/**/*.xml"; + private static final String MYBATIS_CONFIG = "secondaryMybatisConfig"; + private static final String SESSION_FACTORY = "secondarySqlSessionFactory"; + + @Bean(name = BEAN_NAME_GENERATOR) + MybatisBeanNameGenerator mybatisBeanNameGenerator() { + return new MybatisBeanNameGenerator(SecondaryDataSourceConfig.DATABASE); + } + + @Bean(name = MYBATIS_CONFIG) + org.apache.ibatis.session.Configuration mybatisConfiguration() { + var configuration = new org.apache.ibatis.session.Configuration(); + configuration.setMapUnderscoreToCamelCase(true); + configuration.setCallSettersOnNulls(true); + configuration.setReturnInstanceForEmptyRow(true); + configuration.setJdbcTypeForNull(JdbcType.NULL); + return configuration; + } + + @Bean(name = SESSION_FACTORY) + SqlSessionFactory sqlSessionFactory(@Qualifier(SecondaryDataSourceConfig.DATASOURCE) DataSource dataSource) throws Exception { + var sessionFactory = new SqlSessionFactoryBean(); + sessionFactory.setConfiguration(mybatisConfiguration()); + sessionFactory.setDataSource(dataSource); + sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_RESOURCES)); + sessionFactory.setTypeAliasesPackage(ALIASES_PACKAGE); + sessionFactory.setVfs(SpringBootVFS.class); + return sessionFactory.getObject(); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/db/orm/mybatis/util/MybatisBeanNameGenerator.java b/batch-quartz/src/main/java/com/spring/infra/db/orm/mybatis/util/MybatisBeanNameGenerator.java new file mode 100644 index 0000000..9ff9fd7 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/db/orm/mybatis/util/MybatisBeanNameGenerator.java @@ -0,0 +1,41 @@ +package com.spring.infra.db.orm.mybatis.util; + +import java.util.Optional; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.annotation.AnnotationBeanNameGenerator; +import org.springframework.lang.NonNull; + +import com.spring.infra.db.annotation.DatabaseSelector; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class MybatisBeanNameGenerator extends AnnotationBeanNameGenerator { + + private final String dbName; + + @Override + @NonNull + public String generateBeanName(@NonNull BeanDefinition definition, @NonNull BeanDefinitionRegistry registry) { + return Optional.ofNullable(definition.getBeanClassName()) + .flatMap(className -> { + try { + var beanClass = Class.forName(className); + var annotation = beanClass.getAnnotation(DatabaseSelector.class); + if (annotation == null) { + return Optional.of(super.generateBeanName(definition, registry)); + } + if (dbName.equals(annotation.value())) { + return Optional.of(super.generateBeanName(definition, registry)); + } + return Optional.empty(); + } catch (ClassNotFoundException e) { + return Optional.empty(); + } + }) + .orElse(""); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzConfig.java b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzConfig.java new file mode 100644 index 0000000..1d8bce9 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzConfig.java @@ -0,0 +1,80 @@ +package com.spring.infra.quartz; + +import javax.sql.DataSource; + +import org.quartz.spi.JobFactory; +import org.quartz.spi.TriggerFiredBundle; +import org.springframework.batch.core.configuration.JobRegistry; +import org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; +import org.springframework.scheduling.quartz.SpringBeanJobFactory; +import org.springframework.transaction.PlatformTransactionManager; + +import lombok.RequiredArgsConstructor; + +@Configuration +@RequiredArgsConstructor +public class QuartzConfig { + + private final QuartzProperties quartzProperties; + private final DataSource dataSource; + private final PlatformTransactionManager transactionManager; + + /** + * JobRegistry 에 Job 을 자동으로 등록하기 위한 설정. + * + * @param jobRegistry ths Spring Batch Job Registry + * @return JobRegistry BeanPostProcessor + */ + // @Bean + // JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor(JobRegistry jobRegistry) { + // var jobRegistryBeanPostProcessor = new JobRegistryBeanPostProcessor(); + // jobRegistryBeanPostProcessor.setJobRegistry(jobRegistry); + // return jobRegistryBeanPostProcessor; + // } + + /** + * Quartz Schedule Job 에 의존성 주입 + * + * @param beanFactory application context beanFactory + * @return the job factory + */ + @Bean + JobFactory jobFactory(AutowireCapableBeanFactory beanFactory) { + return new SpringBeanJobFactory(){ + @Override + protected @NonNull Object createJobInstance(@NonNull final TriggerFiredBundle bundle) throws Exception { + var job = super.createJobInstance(bundle); + beanFactory.autowireBean(job); + return job; + } + }; + } + + /** + * Scheduler 전체를 관리하는 Manager. + * + * @param datasource Spring datasource + * @param quartzProperties quartz config + * @return the scheduler factory bean + * @throws Exception the exception + */ + @Bean + SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory) throws Exception { + var factory = new SchedulerFactoryBean(); + factory.setSchedulerName("SampleProject-0.0.1"); + factory.setQuartzProperties(quartzProperties.toProperties()); + factory.setOverwriteExistingJobs(true); //Job Detail 데이터 Overwrite 유무 + factory.setDataSource(dataSource); //Schedule 관리를 Spring Datasource 에 위임 + factory.setTransactionManager(transactionManager); + factory.setJobFactory(jobFactory); + factory.setAutoStartup(true); + factory.setWaitForJobsToCompleteOnShutdown(true); + return factory; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJob.java b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJob.java new file mode 100644 index 0000000..eb7cb31 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJob.java @@ -0,0 +1,13 @@ +package com.spring.infra.quartz; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface QuartzJob { + String name(); + String cronExpression(); +} diff --git a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobLauncher.java b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobLauncher.java new file mode 100644 index 0000000..4b31927 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobLauncher.java @@ -0,0 +1,44 @@ +package com.spring.infra.quartz; + +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.configuration.JobLocator; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.lang.NonNull; +import org.springframework.scheduling.quartz.QuartzJobBean; + +public class QuartzJobLauncher extends QuartzJobBean { + + private String jobName; + private JobLauncher jobLauncher; + private JobLocator jobLocator; + + public void setJobName(String jobName) { + this.jobName = jobName; + } + + public void setJobLauncher(JobLauncher jobLauncher) { + this.jobLauncher = jobLauncher; + } + + public void setJobLocator(JobLocator jobLocator) { + this.jobLocator = jobLocator; + } + + @Override + protected void executeInternal(@NonNull JobExecutionContext context) throws JobExecutionException { + try { + Job job = jobLocator.getJob(jobName); + JobParameters params = new JobParametersBuilder() + .addString("JobID", String.valueOf(System.currentTimeMillis())) + .toJobParameters(); + jobLauncher.run(job, params); + } catch (Exception e) { + throw new JobExecutionException(e); + } + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobRegistrar.java b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobRegistrar.java new file mode 100644 index 0000000..2c19350 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobRegistrar.java @@ -0,0 +1,80 @@ +package com.spring.infra.quartz; + +import java.lang.reflect.Method; + +import org.quartz.CronScheduleBuilder; +import org.quartz.JobBuilder; +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; +import org.springframework.batch.core.configuration.JobLocator; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +@Component +public class QuartzJobRegistrar implements BeanPostProcessor, ApplicationContextAware { + + private ApplicationContext applicationContext; + private Scheduler scheduler; + + public QuartzJobRegistrar(Scheduler scheduler) { + this.scheduler = scheduler; + } + + @Override + public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public Object postProcessAfterInitialization(@NonNull Object bean, @NonNull String beanName) throws BeansException { + Class beanClass = bean.getClass(); + for (Method method : beanClass.getMethods()) { + QuartzJob quartzJobAnnotation = method.getAnnotation(QuartzJob.class); + if (quartzJobAnnotation != null) { + registerQuartzJob(quartzJobAnnotation, method.getName()); + } + } + return bean; + } + + private void registerQuartzJob(QuartzJob quartzJobAnnotation, String jobName) { + try { + JobDetail jobDetail = createJobDetail(quartzJobAnnotation, jobName); + Trigger trigger = createTrigger(quartzJobAnnotation, jobDetail); + scheduler.scheduleJob(jobDetail, trigger); + } catch (SchedulerException e) { + throw new RuntimeException("Error scheduling Quartz job", e); + } + } + + private JobDetail createJobDetail(QuartzJob quartzJobAnnotation, String jobName) { + JobDataMap jobDataMap = new JobDataMap(); + jobDataMap.put("jobName", jobName); + jobDataMap.put("jobLauncher", applicationContext.getBean(JobLauncher.class)); + jobDataMap.put("jobLocator", applicationContext.getBean(JobLocator.class)); + + return JobBuilder.newJob(QuartzJobLauncher.class) + .withIdentity(quartzJobAnnotation.name()) + .setJobData(jobDataMap) + .storeDurably() + .build(); + } + + private Trigger createTrigger(QuartzJob quartzJobAnnotation, JobDetail jobDetail) { + return TriggerBuilder.newTrigger() + .forJob(jobDetail) + .withIdentity(quartzJobAnnotation.name() + "Trigger") + .withSchedule(CronScheduleBuilder.cronSchedule(quartzJobAnnotation.cronExpression())) + .build(); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzProperties.java b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzProperties.java new file mode 100644 index 0000000..b062325 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzProperties.java @@ -0,0 +1,87 @@ +package com.spring.infra.quartz; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +@RequiredArgsConstructor +@ConfigurationProperties(prefix = "spring.quartz.properties.org.quartz") +public class QuartzProperties { + + private static final String PREFIX = "org.quartz"; + + private final JobStore jobStore; + private final Scheduler scheduler; + private final ThreadPool threadPool; + + @Getter + @RequiredArgsConstructor + public static class JobStore { + private final String tablePrefix; + private final String isClustered; + private final int clusterCheckinInterval; + } + + @Getter + @RequiredArgsConstructor + public static class Scheduler { + private final String instanceId; + private final String instanceName; + } + + @Getter + @RequiredArgsConstructor + public static class ThreadPool { + private final int threadCount; + private final int threadPriority; + } + + public Properties toProperties() { + Properties properties = new Properties(); + addProperties(PREFIX, this, properties); + return properties; + } + + private void addProperties(String prefix, Object object, Properties properties) { + Arrays.stream(object.getClass().getDeclaredFields()) + .filter(field -> !Modifier.isStatic(field.getModifiers())) + .forEach(field -> { + setProperties(prefix, object, properties, field); + }); + } + + private void setProperties(String prefix, Object object, Properties properties, Field field) { + try { + Object value = field.get(object); + if (value != null) { + String fieldName = field.getName(); + String key = prefix + "." + fieldName; + if (isSimpleType(field.getType())) { + properties.setProperty(key, String.valueOf(value)); + } else { + addProperties(key, value, properties); + } + } + } catch (IllegalAccessException e) { + log.error("Error getting property value for {}", field.getName(), e); + } + } + + private boolean isSimpleType(Class type) { + return type.isPrimitive() + || String.class == type + || Number.class.isAssignableFrom(type) + || Boolean.class == type + || Character.class == type; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/security/config/SecurityConfig.java b/batch-quartz/src/main/java/com/spring/infra/security/config/SecurityConfig.java new file mode 100644 index 0000000..8021605 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/security/config/SecurityConfig.java @@ -0,0 +1,71 @@ +package com.spring.infra.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; +import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig; +import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import com.spring.infra.security.filter.JwtAuthenticationFilter; +import com.spring.infra.security.handler.JwtAccessDeniedHandler; +import com.spring.infra.security.handler.JwtAuthenticationEntryPoint; +import com.spring.infra.security.jwt.JwtTokenService; + +import lombok.RequiredArgsConstructor; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + public static final String PERMITTED_URI[] = {"/api/auth/**", "/signIn"}; + + @Bean + SecurityFilterChain securityFilterChain( + HttpSecurity http, + JwtTokenService tokenService, + JwtAuthenticationEntryPoint authenticationEntryPoint, + JwtAccessDeniedHandler accessDeniedHandler) throws Exception + { + http + .headers(headers -> headers.frameOptions(FrameOptionsConfig::sameOrigin)) + .csrf(CsrfConfigurer::disable) + .httpBasic(HttpBasicConfigurer::disable) + .formLogin(FormLoginConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers(PERMITTED_URI).permitAll() + .anyRequest().authenticated() + ) + .logout((logout) -> logout + .logoutSuccessUrl("/signIn") + .invalidateHttpSession(true)) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .addFilterBefore(new JwtAuthenticationFilter(tokenService), UsernamePasswordAuthenticationFilter.class) + .exceptionHandling(ex -> ex.authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler)); + return http.build(); + } + + @Bean + WebSecurityCustomizer ignoringCustomizer() { + return (web) -> web.ignoring().requestMatchers("/h2-console/**"); + } + + @Bean + PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/security/domain/UserPrincipal.java b/batch-quartz/src/main/java/com/spring/infra/security/domain/UserPrincipal.java new file mode 100644 index 0000000..feab821 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/security/domain/UserPrincipal.java @@ -0,0 +1,63 @@ +package com.spring.infra.security.domain; + +import java.util.Collection; +import java.util.stream.Collectors; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import com.spring.domain.user.entity.AppUser; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public final class UserPrincipal implements UserDetails { + + private final AppUser appUser; + + public static UserPrincipal valueOf(AppUser appUser) { + return new UserPrincipal(appUser); + } + + @Override + public Collection getAuthorities() { + return appUser.getAppUserRoleMap().stream() + .map(role -> role.getAppUserRole().getRoleType()) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public String getPassword() { + return null; + } + + @Override + public String getUsername() { + return appUser.getLoginId(); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/security/filter/JwtAuthenticationFilter.java b/batch-quartz/src/main/java/com/spring/infra/security/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..fb0bb81 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/security/filter/JwtAuthenticationFilter.java @@ -0,0 +1,54 @@ +package com.spring.infra.security.filter; + +import java.io.IOException; + +import org.springframework.lang.NonNull; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import com.spring.infra.security.jwt.JwtTokenRule; +import com.spring.infra.security.jwt.JwtTokenService; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public final class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenService jwtTokenService; + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) + throws ServletException, IOException { + + String accessToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.ACCESS_PREFIX); + if (jwtTokenService.validateAccessToken(accessToken)) { + setAuthenticationToContext(accessToken); + filterChain.doFilter(request, response); + return; + } + + String refreshToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.REFRESH_PREFIX); + if (jwtTokenService.validateRefreshToken(refreshToken)) { + Authentication authentication = jwtTokenService.getAuthentication(refreshToken); + String reissuedAccessToken = jwtTokenService.generateAccessToken(response, authentication); + jwtTokenService.generateRefreshToken(response, authentication); + setAuthenticationToContext(reissuedAccessToken); + filterChain.doFilter(request, response); + return; + } + + jwtTokenService.deleteCookie(response); + + } + + private void setAuthenticationToContext(String accessToken) { + Authentication authentication = jwtTokenService.getAuthentication(accessToken); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/security/handler/JwtAccessDeniedHandler.java b/batch-quartz/src/main/java/com/spring/infra/security/handler/JwtAccessDeniedHandler.java new file mode 100644 index 0000000..06a04a6 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/security/handler/JwtAccessDeniedHandler.java @@ -0,0 +1,25 @@ +package com.spring.infra.security.handler; + +import java.io.IOException; + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * 유저 정보는 있으나 자원에 접근할 수 있는 권한이 없는 경우 : SC_FORBIDDEN (403) 응답 + */ +@Component +public class JwtAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException, ServletException { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/security/handler/JwtAuthenticationEntryPoint.java b/batch-quartz/src/main/java/com/spring/infra/security/handler/JwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..f687b75 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/security/handler/JwtAuthenticationEntryPoint.java @@ -0,0 +1,25 @@ +package com.spring.infra.security.handler; + +import java.io.IOException; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * 유저 정보 없이 접근한 경우 : SC_UNAUTHORIZED (401) 응답 + */ +@Component +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtProperties.java b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtProperties.java new file mode 100644 index 0000000..93adcc1 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtProperties.java @@ -0,0 +1,23 @@ +package com.spring.infra.security.jwt; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@ConfigurationProperties(prefix = "jwt") +@RequiredArgsConstructor +public final class JwtProperties { + + private final TokenProperties accessToken; + private final TokenProperties refreshToken; + + @Getter + @RequiredArgsConstructor + public static class TokenProperties { + private final String secret; + private final long expiration; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenGenerator.java b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenGenerator.java new file mode 100644 index 0000000..07b2fd6 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenGenerator.java @@ -0,0 +1,60 @@ +package com.spring.infra.security.jwt; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.crypto.spec.SecretKeySpec; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class JwtTokenGenerator { + + private final JwtProperties jwtProperties; + + public String generateAccessToken(Authentication authentication) { + return Jwts.builder() + .setHeader(createHeader()) + .setClaims(createClaims(authentication)) + .setSubject(authentication.getName()) + .setExpiration(Date.from(Instant.now().plus(jwtProperties.getAccessToken().getExpiration(), ChronoUnit.HOURS))) + .signWith(new SecretKeySpec(jwtProperties.getAccessToken().getSecret().getBytes(), SignatureAlgorithm.HS512.getJcaName())) + .compact(); + } + + public String generateRefreshToken(Authentication authentication) { + return Jwts.builder() + .setHeader(createHeader()) + .setSubject(authentication.getName()) + .setExpiration(Date.from(Instant.now().plus(jwtProperties.getRefreshToken().getExpiration(), ChronoUnit.HOURS))) + .signWith(new SecretKeySpec(jwtProperties.getRefreshToken().getSecret().getBytes(), SignatureAlgorithm.HS512.getJcaName())) + .compact(); + } + + private Map createHeader() { + Map header = new HashMap<>(); + header.put("typ", "JWT"); + header.put("alg", "HS512"); + return header; + } + + private Map createClaims(Authentication authentication) { + Map claims = new HashMap<>(); + claims.put(JwtTokenRule.AUTHORITIES_KEY.getValue(), authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toList())); + return claims; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenRule.java b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenRule.java new file mode 100644 index 0000000..a5e1ef7 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenRule.java @@ -0,0 +1,19 @@ +package com.spring.infra.security.jwt; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum JwtTokenRule { + + JWT_ISSUE_HEADER("Set-Cookie"), + JWT_RESOLVE_HEADER("Cookie"), + ACCESS_PREFIX("access"), + REFRESH_PREFIX("refresh"), + BEARER_PREFIX("Bearer "), + AUTHORITIES_KEY("auth"); + + private final String value; + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenService.java b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenService.java new file mode 100644 index 0000000..4ec3cf1 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenService.java @@ -0,0 +1,106 @@ +package com.spring.infra.security.jwt; + +import java.security.Key; + +import org.springframework.http.ResponseCookie; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import com.spring.infra.security.service.UserPrincipalService; + +import io.jsonwebtoken.Jwts; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Service +public class JwtTokenService { + + private final JwtTokenUtil jwtTokenUtil; + private final JwtTokenGenerator jwtTokenGenerator; + private final UserPrincipalService userPrincipalService; + + private final Key ACCESS_SECRET_KEY; + private final Key REFRESH_SECRET_KEY; + private final long ACCESS_EXPIRATION; + private final long REFRESH_EXPIRATION; + + public JwtTokenService( + JwtTokenUtil jwtTokenUtil, + JwtTokenGenerator jwtTokenGenerator, + UserPrincipalService userPrincipalService, + JwtProperties jwtProperties + ) { + this.jwtTokenUtil = jwtTokenUtil; + this.jwtTokenGenerator = jwtTokenGenerator; + this.userPrincipalService = userPrincipalService; + this.ACCESS_SECRET_KEY = jwtTokenUtil.getSigningKey(jwtProperties.getAccessToken().getSecret()); + this.REFRESH_SECRET_KEY = jwtTokenUtil.getSigningKey(jwtProperties.getRefreshToken().getSecret()); + this.ACCESS_EXPIRATION = jwtProperties.getAccessToken().getExpiration(); + this.REFRESH_EXPIRATION = jwtProperties.getRefreshToken().getExpiration(); + } + + public String generateAccessToken(HttpServletResponse response, Authentication authentication) { + String accessToken = jwtTokenGenerator.generateAccessToken(authentication); + ResponseCookie cookie = setTokenToCookie(JwtTokenRule.ACCESS_PREFIX.getValue(), accessToken, ACCESS_EXPIRATION / 1000); + response.addHeader(JwtTokenRule.JWT_ISSUE_HEADER.getValue(), cookie.toString()); + return accessToken; + } + + public String generateRefreshToken(HttpServletResponse response, Authentication authentication) { + String refreshToken = jwtTokenGenerator.generateRefreshToken(authentication); + ResponseCookie cookie = setTokenToCookie(JwtTokenRule.REFRESH_PREFIX.getValue(), refreshToken, REFRESH_EXPIRATION / 1000); + response.addHeader(JwtTokenRule.JWT_ISSUE_HEADER.getValue(), cookie.toString()); + return refreshToken; + } + + private ResponseCookie setTokenToCookie(String tokenPrefix, String token, long maxAgeSeconds) { + return ResponseCookie.from(tokenPrefix, token) + .path("/") + .maxAge(maxAgeSeconds) + .httpOnly(true) + .sameSite("None") + .secure(true) + .build(); + } + + public boolean validateAccessToken(String token) { + return jwtTokenUtil.getTokenStatus(token, ACCESS_SECRET_KEY) == JwtTokenStatus.AUTHENTICATED; + } + + public boolean validateRefreshToken(String token) { + return jwtTokenUtil.getTokenStatus(token, REFRESH_SECRET_KEY) == JwtTokenStatus.AUTHENTICATED; + } + + public String resolveTokenFromCookie(HttpServletRequest request, JwtTokenRule tokenPrefix) { + Cookie[] cookies = request.getCookies(); + if (cookies == null) { + throw new RuntimeException("JWT_TOKEN_NOT_FOUND"); + } + return jwtTokenUtil.resolveTokenFromCookie(cookies, tokenPrefix); + } + + public Authentication getAuthentication(String token) { + UserDetails principal = userPrincipalService.loadUserByUsername(getUserPk(token, ACCESS_SECRET_KEY)); + return new UsernamePasswordAuthenticationToken(principal, "", principal.getAuthorities()); + } + + private String getUserPk(String token, Key secretKey) { + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody() + .getSubject(); + } + + public void deleteCookie(HttpServletResponse response) { + Cookie accessCookie = jwtTokenUtil.resetToken(JwtTokenRule.ACCESS_PREFIX); + Cookie refreshCookie = jwtTokenUtil.resetToken(JwtTokenRule.REFRESH_PREFIX); + response.addCookie(accessCookie); + response.addCookie(refreshCookie); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenStatus.java b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenStatus.java new file mode 100644 index 0000000..cd4cb81 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenStatus.java @@ -0,0 +1,12 @@ +package com.spring.infra.security.jwt; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum JwtTokenStatus { + AUTHENTICATED, + EXPIRED, + INVALID +} diff --git a/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenUtil.java b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenUtil.java new file mode 100644 index 0000000..24fe419 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenUtil.java @@ -0,0 +1,61 @@ +package com.spring.infra.security.jwt; + +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Arrays; +import java.util.Base64; + +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.Cookie; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class JwtTokenUtil { + + public JwtTokenStatus getTokenStatus(String token, Key secretKey) { + try { + Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token); + return JwtTokenStatus.AUTHENTICATED; + } catch (ExpiredJwtException | IllegalArgumentException e) { + log.error("만료된 JWT 토큰입니다."); + return JwtTokenStatus.EXPIRED; + } catch (JwtException e) { + log.error("JWT 토큰이 잘못되었습니다."); + return JwtTokenStatus.INVALID; + } + } + + public String resolveTokenFromCookie(Cookie[] cookies, JwtTokenRule tokenPrefix) { + return Arrays.stream(cookies) + .filter(cookie -> cookie.getName().equals(tokenPrefix.getValue())) + .findFirst() + .map(Cookie::getValue) + .orElse(""); + } + + public Key getSigningKey(String secretKey) { + String encodedKey = encodeToBase64(secretKey); + return Keys.hmacShaKeyFor(encodedKey.getBytes(StandardCharsets.UTF_8)); + } + + private String encodeToBase64(String secretKey) { + return Base64.getEncoder().encodeToString(secretKey.getBytes()); + } + + public Cookie resetToken(JwtTokenRule tokenPrefix) { + Cookie cookie = new Cookie(tokenPrefix.getValue(), null); + cookie.setMaxAge(0); + cookie.setPath("/"); + return cookie; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/security/service/UserPrincipalService.java b/batch-quartz/src/main/java/com/spring/infra/security/service/UserPrincipalService.java new file mode 100644 index 0000000..22d6b93 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/security/service/UserPrincipalService.java @@ -0,0 +1,29 @@ +package com.spring.infra.security.service; + +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.spring.domain.user.entity.AppUser; +import com.spring.domain.user.repository.AppUserRepository; +import com.spring.infra.security.domain.UserPrincipal; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class UserPrincipalService implements UserDetailsService { + + private final AppUserRepository appUserRepository; + + @Transactional(readOnly = true) + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + AppUser user = appUserRepository.findByLoginId(username) + .orElseThrow(() -> new UsernameNotFoundException("NOT FOUND USER")); + return UserPrincipal.valueOf(user); + } + +} diff --git a/batch-quartz/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/batch-quartz/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000..50472dd --- /dev/null +++ b/batch-quartz/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,147 @@ +{"properties": [ + { + "name": "jwt.secret", + "type": "java.lang.String", + "description": "A description for 'jwt.secret'" + }, + { + "name": "jwt.access-token.expiration", + "type": "java.lang.String", + "description": "A description for 'jwt.access-token.expiration'" + }, + { + "name": "jwt.refresh-token.expiration", + "type": "java.lang.String", + "description": "A description for 'jwt.refresh-token.expiration'" + }, + { + "name": "jwt.access-token.secret", + "type": "java.lang.String", + "description": "A description for 'jwt.access-token.secret'" + }, + { + "name": "jwt.refresh-token.secret", + "type": "java.lang.String", + "description": "A description for 'jwt.refresh-token.secret'" + }, + { + "name": "spring.datasource.mob.driver-class-name", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.mob.driver-class-name'" + }, + { + "name": "spring.datasource.mob.url", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.mob.url'" + }, + { + "name": "spring.datasource.mob.username", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.mob.username'" + }, + { + "name": "spring.datasource.mob.password", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.mob.password'" + }, + { + "name": "spring.datasource.app.hikari.pool-name", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.app.hikari.pool-name'" + }, + { + "name": "spring.datasource.app.hikari.maximum-pool-size", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.app.hikari.maximum-pool-size'" + }, + { + "name": "spring.datasource.app.hikari.minimum-idle", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.app.hikari.minimum-idle'" + }, + { + "name": "spring.datasource.mob.hikari.pool-name", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.mob.hikari.pool-name'" + }, + { + "name": "spring.datasource.mob.hikari.maximum-pool-size", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.mob.hikari.maximum-pool-size'" + }, + { + "name": "spring.datasource.mob.hikari.minimum-idle", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.mob.hikari.minimum-idle'" + }, + { + "name": "spring.datasource.primary.driver-class-name", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.primary.driver-class-name'" + }, + { + "name": "spring.datasource.primary.url", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.primary.url'" + }, + { + "name": "spring.datasource.primary.username", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.primary.username'" + }, + { + "name": "spring.datasource.primary.password", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.primary.password'" + }, + { + "name": "spring.datasource.primary.hikari.pool-name", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.primary.hikari.pool-name'" + }, + { + "name": "spring.datasource.primary.hikari.maximum-pool-size", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.primary.hikari.maximum-pool-size'" + }, + { + "name": "spring.datasource.primary.hikari.minimum-idle", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.primary.hikari.minimum-idle'" + }, + { + "name": "spring.datasource.secondary.driver-class-name", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.secondary.driver-class-name'" + }, + { + "name": "spring.datasource.secondary.url", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.secondary.url'" + }, + { + "name": "spring.datasource.secondary.username", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.secondary.username'" + }, + { + "name": "spring.datasource.secondary.password", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.secondary.password'" + }, + { + "name": "spring.datasource.secondary.hikari.pool-name", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.secondary.hikari.pool-name'" + }, + { + "name": "spring.datasource.secondary.hikari.maximum-pool-size", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.secondary.hikari.maximum-pool-size'" + }, + { + "name": "spring.datasource.secondary.hikari.minimum-idle", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.secondary.hikari.minimum-idle'" + } +]} \ No newline at end of file diff --git a/batch-quartz/src/main/resources/application.yml b/batch-quartz/src/main/resources/application.yml new file mode 100644 index 0000000..ee24177 --- /dev/null +++ b/batch-quartz/src/main/resources/application.yml @@ -0,0 +1,85 @@ +server: + port: 8081 + +spring: + application: + name: spring-batch-quartz + datasource: + primary: + driver-class-name: org.h2.Driver + url: 'jdbc:h2:mem:app' + username: mindol1004 + password: 1111 + hikari: + pool-name: HikariPool-1 + maximum-pool-size: 10 + minimum-idle: 5 + secondary: + driver-class-name: org.h2.Driver + url: 'jdbc:h2:mem:mob' + username: mindol1004 + password: 1111 + hikari: + pool-name: HikariPool-2 + maximum-pool-size: 10 + minimum-idle: 5 +# sql: +# init: +# mode: always +# schema-locations: +# - classpath:batch-schema.sql +# - classpath:quartz-schema.sql + + jpa: + database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: create + properties: + hibernate: + dialect: org.hibernate.dialect.H2Dialect + "[format_sql]": true # 쿼리 로그 포맷 (정렬) + "[show_sql]": true # 쿼리 로그 출력 + naming: + physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + show-sql: true + + batch: + jdbc: + initialize-schema: never + + quartz: + jdbc: + initialize-schema: never + wait-for-jobs-to-complete-on-shutdown: true + job-store-type: jdbc + properties: + org: + quartz: + scheduler: + instanceName: BatchQuartzScheduler + instanceId: AUTO + jobStore: + class: org.quartz.impl.jdbcjobstore.JobStoreTX + driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate + tablePrefix: QRTZ_ + isClustered: true + clusterCheckinInterval: 20000 + threadPool: + class: org.quartz.simpl.SimpleThreadPool + threadCount: 10 + threadPriority: 5 + + h2: + console: # H2 DB를 웹에서 관리할 수 있는 기능 + enabled: true # H2 Console 사용 여부 + path: /h2-console # H2 Console 접속 주소 + settings: + web-allow-others: true + +jwt: + access-token: + secret: bnhjdXMyLjAtcGxhdGZvcm0tcHJvamVjdC13aXRoLXNwcmluZy1ib290 + expiration: 900 # 15분 + refresh-token: + secret: bnhjdXMyLjAtcGxhdGZvcm0tcHJvamVjdC13aXRoLXNwcmluZy1ib290 + expiration: 604800 # 7일 \ No newline at end of file diff --git a/batch-quartz/src/main/resources/batch-schema.sql b/batch-quartz/src/main/resources/batch-schema.sql new file mode 100644 index 0000000..193ded9 --- /dev/null +++ b/batch-quartz/src/main/resources/batch-schema.sql @@ -0,0 +1,78 @@ +-- Batch + +CREATE TABLE BATCH_JOB_INSTANCE ( + JOB_INSTANCE_ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY , + VERSION BIGINT , + JOB_NAME VARCHAR(100) NOT NULL, + JOB_KEY VARCHAR(32) NOT NULL, + constraint JOB_INST_UN unique (JOB_NAME, JOB_KEY) +) ; + +CREATE TABLE BATCH_JOB_EXECUTION ( + JOB_EXECUTION_ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY , + VERSION BIGINT , + JOB_INSTANCE_ID BIGINT NOT NULL, + CREATE_TIME TIMESTAMP(9) NOT NULL, + START_TIME TIMESTAMP(9) DEFAULT NULL , + END_TIME TIMESTAMP(9) DEFAULT NULL , + STATUS VARCHAR(10) , + EXIT_CODE VARCHAR(2500) , + EXIT_MESSAGE VARCHAR(2500) , + LAST_UPDATED TIMESTAMP(9), + constraint JOB_INST_EXEC_FK foreign key (JOB_INSTANCE_ID) + references BATCH_JOB_INSTANCE(JOB_INSTANCE_ID) +) ; + +CREATE TABLE BATCH_JOB_EXECUTION_PARAMS ( + JOB_EXECUTION_ID BIGINT NOT NULL , + PARAMETER_NAME VARCHAR(100) NOT NULL , + PARAMETER_TYPE VARCHAR(100) NOT NULL , + PARAMETER_VALUE VARCHAR(2500) , + IDENTIFYING CHAR(1) NOT NULL , + constraint JOB_EXEC_PARAMS_FK foreign key (JOB_EXECUTION_ID) + references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID) +) ; + +CREATE TABLE BATCH_STEP_EXECUTION ( + STEP_EXECUTION_ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY , + VERSION BIGINT NOT NULL, + STEP_NAME VARCHAR(100) NOT NULL, + JOB_EXECUTION_ID BIGINT NOT NULL, + CREATE_TIME TIMESTAMP(9) NOT NULL, + START_TIME TIMESTAMP(9) DEFAULT NULL , + END_TIME TIMESTAMP(9) DEFAULT NULL , + STATUS VARCHAR(10) , + COMMIT_COUNT BIGINT , + READ_COUNT BIGINT , + FILTER_COUNT BIGINT , + WRITE_COUNT BIGINT , + READ_SKIP_COUNT BIGINT , + WRITE_SKIP_COUNT BIGINT , + PROCESS_SKIP_COUNT BIGINT , + ROLLBACK_COUNT BIGINT , + EXIT_CODE VARCHAR(2500) , + EXIT_MESSAGE VARCHAR(2500) , + LAST_UPDATED TIMESTAMP(9), + constraint JOB_EXEC_STEP_FK foreign key (JOB_EXECUTION_ID) + references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID) +) ; + +CREATE TABLE BATCH_STEP_EXECUTION_CONTEXT ( + STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY, + SHORT_CONTEXT VARCHAR(2500) NOT NULL, + SERIALIZED_CONTEXT LONGVARCHAR , + constraint STEP_EXEC_CTX_FK foreign key (STEP_EXECUTION_ID) + references BATCH_STEP_EXECUTION(STEP_EXECUTION_ID) +) ; + +CREATE TABLE BATCH_JOB_EXECUTION_CONTEXT ( + JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY, + SHORT_CONTEXT VARCHAR(2500) NOT NULL, + SERIALIZED_CONTEXT LONGVARCHAR , + constraint JOB_EXEC_CTX_FK foreign key (JOB_EXECUTION_ID) + references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID) +) ; + +CREATE SEQUENCE BATCH_STEP_EXECUTION_SEQ; +CREATE SEQUENCE BATCH_JOB_EXECUTION_SEQ; +CREATE SEQUENCE BATCH_JOB_SEQ; diff --git a/batch-quartz/src/main/resources/quartz-schema.sql b/batch-quartz/src/main/resources/quartz-schema.sql new file mode 100644 index 0000000..13b29f9 --- /dev/null +++ b/batch-quartz/src/main/resources/quartz-schema.sql @@ -0,0 +1,238 @@ +CREATE TABLE QRTZ_CALENDARS ( + SCHED_NAME VARCHAR(120) NOT NULL, + CALENDAR_NAME VARCHAR (200) NOT NULL , + CALENDAR IMAGE NOT NULL +); + +CREATE TABLE QRTZ_CRON_TRIGGERS ( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR (200) NOT NULL , + TRIGGER_GROUP VARCHAR (200) NOT NULL , + CRON_EXPRESSION VARCHAR (120) NOT NULL , + TIME_ZONE_ID VARCHAR (80) +); + +CREATE TABLE QRTZ_FIRED_TRIGGERS ( + SCHED_NAME VARCHAR(120) NOT NULL, + ENTRY_ID VARCHAR (95) NOT NULL , + TRIGGER_NAME VARCHAR (200) NOT NULL , + TRIGGER_GROUP VARCHAR (200) NOT NULL , + INSTANCE_NAME VARCHAR (200) NOT NULL , + FIRED_TIME BIGINT NOT NULL , + SCHED_TIME BIGINT NOT NULL , + PRIORITY INTEGER NOT NULL , + STATE VARCHAR (16) NOT NULL, + JOB_NAME VARCHAR (200) NULL , + JOB_GROUP VARCHAR (200) NULL , + IS_NONCONCURRENT BOOLEAN NULL , + REQUESTS_RECOVERY BOOLEAN NULL +); + +CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS ( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_GROUP VARCHAR (200) NOT NULL +); + +CREATE TABLE QRTZ_SCHEDULER_STATE ( + SCHED_NAME VARCHAR(120) NOT NULL, + INSTANCE_NAME VARCHAR (200) NOT NULL , + LAST_CHECKIN_TIME BIGINT NOT NULL , + CHECKIN_INTERVAL BIGINT NOT NULL +); + +CREATE TABLE QRTZ_LOCKS ( + SCHED_NAME VARCHAR(120) NOT NULL, + LOCK_NAME VARCHAR (40) NOT NULL +); + +CREATE TABLE QRTZ_JOB_DETAILS ( + SCHED_NAME VARCHAR(120) NOT NULL, + JOB_NAME VARCHAR (200) NOT NULL , + JOB_GROUP VARCHAR (200) NOT NULL , + DESCRIPTION VARCHAR (250) NULL , + JOB_CLASS_NAME VARCHAR (250) NOT NULL , + IS_DURABLE BOOLEAN NOT NULL , + IS_NONCONCURRENT BOOLEAN NOT NULL , + IS_UPDATE_DATA BOOLEAN NOT NULL , + REQUESTS_RECOVERY BOOLEAN NOT NULL , + JOB_DATA IMAGE NULL +); + +CREATE TABLE QRTZ_SIMPLE_TRIGGERS ( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR (200) NOT NULL , + TRIGGER_GROUP VARCHAR (200) NOT NULL , + REPEAT_COUNT BIGINT NOT NULL , + REPEAT_INTERVAL BIGINT NOT NULL , + TIMES_TRIGGERED BIGINT NOT NULL +); + +CREATE TABLE QRTZ_SIMPROP_TRIGGERS ( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + STR_PROP_1 VARCHAR(512) NULL, + STR_PROP_2 VARCHAR(512) NULL, + STR_PROP_3 VARCHAR(512) NULL, + INT_PROP_1 INTEGER NULL, + INT_PROP_2 INTEGER NULL, + LONG_PROP_1 BIGINT NULL, + LONG_PROP_2 BIGINT NULL, + DEC_PROP_1 NUMERIC(13,4) NULL, + DEC_PROP_2 NUMERIC(13,4) NULL, + BOOL_PROP_1 BOOLEAN NULL, + BOOL_PROP_2 BOOLEAN NULL +); + +CREATE TABLE QRTZ_BLOB_TRIGGERS ( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR (200) NOT NULL , + TRIGGER_GROUP VARCHAR (200) NOT NULL , + BLOB_DATA IMAGE NULL +); + +CREATE TABLE QRTZ_TRIGGERS ( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR (200) NOT NULL , + TRIGGER_GROUP VARCHAR (200) NOT NULL , + JOB_NAME VARCHAR (200) NOT NULL , + JOB_GROUP VARCHAR (200) NOT NULL , + DESCRIPTION VARCHAR (250) NULL , + NEXT_FIRE_TIME BIGINT NULL , + PREV_FIRE_TIME BIGINT NULL , + PRIORITY INTEGER NULL , + TRIGGER_STATE VARCHAR (16) NOT NULL , + TRIGGER_TYPE VARCHAR (8) NOT NULL , + START_TIME BIGINT NOT NULL , + END_TIME BIGINT NULL , + CALENDAR_NAME VARCHAR (200) NULL , + MISFIRE_INSTR SMALLINT NULL , + JOB_DATA IMAGE NULL +); + +ALTER TABLE QRTZ_CALENDARS ADD + CONSTRAINT PK_QRTZ_CALENDARS PRIMARY KEY + ( + SCHED_NAME, + CALENDAR_NAME + ); + +ALTER TABLE QRTZ_CRON_TRIGGERS ADD + CONSTRAINT PK_QRTZ_CRON_TRIGGERS PRIMARY KEY + ( + SCHED_NAME, + TRIGGER_NAME, + TRIGGER_GROUP + ); + +ALTER TABLE QRTZ_FIRED_TRIGGERS ADD + CONSTRAINT PK_QRTZ_FIRED_TRIGGERS PRIMARY KEY + ( + SCHED_NAME, + ENTRY_ID + ); + +ALTER TABLE QRTZ_PAUSED_TRIGGER_GRPS ADD + CONSTRAINT PK_QRTZ_PAUSED_TRIGGER_GRPS PRIMARY KEY + ( + SCHED_NAME, + TRIGGER_GROUP + ); + +ALTER TABLE QRTZ_SCHEDULER_STATE ADD + CONSTRAINT PK_QRTZ_SCHEDULER_STATE PRIMARY KEY + ( + SCHED_NAME, + INSTANCE_NAME + ); + +ALTER TABLE QRTZ_LOCKS ADD + CONSTRAINT PK_QRTZ_LOCKS PRIMARY KEY + ( + SCHED_NAME, + LOCK_NAME + ); + +ALTER TABLE QRTZ_JOB_DETAILS ADD + CONSTRAINT PK_QRTZ_JOB_DETAILS PRIMARY KEY + ( + SCHED_NAME, + JOB_NAME, + JOB_GROUP + ); + +ALTER TABLE QRTZ_SIMPLE_TRIGGERS ADD + CONSTRAINT PK_QRTZ_SIMPLE_TRIGGERS PRIMARY KEY + ( + SCHED_NAME, + TRIGGER_NAME, + TRIGGER_GROUP + ); + +ALTER TABLE QRTZ_SIMPROP_TRIGGERS ADD + CONSTRAINT PK_QRTZ_SIMPROP_TRIGGERS PRIMARY KEY + ( + SCHED_NAME, + TRIGGER_NAME, + TRIGGER_GROUP + ); + +ALTER TABLE QRTZ_TRIGGERS ADD + CONSTRAINT PK_QRTZ_TRIGGERS PRIMARY KEY + ( + SCHED_NAME, + TRIGGER_NAME, + TRIGGER_GROUP + ); + +ALTER TABLE QRTZ_CRON_TRIGGERS ADD + CONSTRAINT FK_QRTZ_CRON_TRIGGERS_QRTZ_TRIGGERS FOREIGN KEY + ( + SCHED_NAME, + TRIGGER_NAME, + TRIGGER_GROUP + ) REFERENCES QRTZ_TRIGGERS ( + SCHED_NAME, + TRIGGER_NAME, + TRIGGER_GROUP + ) ON DELETE CASCADE; + + +ALTER TABLE QRTZ_SIMPLE_TRIGGERS ADD + CONSTRAINT FK_QRTZ_SIMPLE_TRIGGERS_QRTZ_TRIGGERS FOREIGN KEY + ( + SCHED_NAME, + TRIGGER_NAME, + TRIGGER_GROUP + ) REFERENCES QRTZ_TRIGGERS ( + SCHED_NAME, + TRIGGER_NAME, + TRIGGER_GROUP + ) ON DELETE CASCADE; + +ALTER TABLE QRTZ_SIMPROP_TRIGGERS ADD + CONSTRAINT FK_QRTZ_SIMPROP_TRIGGERS_QRTZ_TRIGGERS FOREIGN KEY + ( + SCHED_NAME, + TRIGGER_NAME, + TRIGGER_GROUP + ) REFERENCES QRTZ_TRIGGERS ( + SCHED_NAME, + TRIGGER_NAME, + TRIGGER_GROUP + ) ON DELETE CASCADE; + + +ALTER TABLE QRTZ_TRIGGERS ADD + CONSTRAINT FK_QRTZ_TRIGGERS_QRTZ_JOB_DETAILS FOREIGN KEY + ( + SCHED_NAME, + JOB_NAME, + JOB_GROUP + ) REFERENCES QRTZ_JOB_DETAILS ( + SCHED_NAME, + JOB_NAME, + JOB_GROUP + ); + +COMMIT; \ No newline at end of file diff --git a/batch-quartz/src/main/resources/quartz.yml b/batch-quartz/src/main/resources/quartz.yml new file mode 100644 index 0000000..7c88af7 --- /dev/null +++ b/batch-quartz/src/main/resources/quartz.yml @@ -0,0 +1,40 @@ +spring: + quartz: + scheduler: + instanceName: batch-quartz + instance-id: SYS_PROP + name: BatchQuartzScheduler + +org: + quartz: + jobStore: + tablePrefix: QRTZ_ + isClustered: true + misfireThreshold: 2000 + clusterCheckinInterval: 1000 + class: org.quartz.impl.jdbcjobstore.JobStoreTX + driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate #org.quartz.impl.jdbcjobstore.oracle.OracleDelegate + acquireTriggersWithinLock: true + scheduler: + instance-id: + instanceName: + rmi: + export: false + proxy: false + batchTriggerAcquisitionMaxCount: 20 + idleWaitTime: 1000 + skipUpdateCheck: true + threadPool: + class: org.quartz.simpl.SimpleThreadPool + threadCount: 10 + threadPriority: 5 + threadsInheritContextClassLoaderOfInitializingThread: true + threadNamePrefix: BatchQuartz +# dataSource: +# nxcus: +# driver: org.h2.Driver #oracle.jdbc.driver.OracleDriver +# URL: 'jdbc:h2:mem:test' #jdbc:oracle:thin:@polarbear:1521:dev +# user: mindol1004 +# password: 1111 +# maxConnections: 5 +# validationQuery: select 0 from dual \ No newline at end of file diff --git a/batch-quartz/src/test/java/com/spring/batch_quartz/BatchQuartzApplicationTests.java b/batch-quartz/src/test/java/com/spring/batch_quartz/BatchQuartzApplicationTests.java new file mode 100644 index 0000000..b1d3a26 --- /dev/null +++ b/batch-quartz/src/test/java/com/spring/batch_quartz/BatchQuartzApplicationTests.java @@ -0,0 +1,13 @@ +package com.spring.batch_quartz; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class BatchQuartzApplicationTests { + + @Test + void contextLoads() { + } + +}