commit
This commit is contained in:
@@ -42,6 +42,14 @@
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-okhttp</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
|
||||
@@ -43,5 +43,10 @@ public class BizBaseException extends RuntimeException {
|
||||
super(exceptionRule.getMessage());
|
||||
this.errorRule = exceptionRule;
|
||||
}
|
||||
|
||||
public BizBaseException(String message) {
|
||||
super(message);
|
||||
this.errorRule = ExceptionRule.SYSTEM_ERROR.message(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.spring.common.error;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
@@ -45,5 +48,17 @@ public class GlobalExceptionHandler {
|
||||
public BizErrorResponse methodArgumentNotValidException(MethodArgumentNotValidException e) {
|
||||
return BizErrorResponse.fromFieldError(e.getFieldErrors());
|
||||
}
|
||||
|
||||
@ExceptionHandler(DataAccessException.class)
|
||||
public BizErrorResponse handleDataAccessException(DataAccessException e) {
|
||||
Throwable cause = e.getMostSpecificCause();
|
||||
return BizErrorResponse.valueOf(
|
||||
new BizBaseException(
|
||||
Optional.ofNullable(cause)
|
||||
.map(Throwable::getMessage)
|
||||
.orElse(ExceptionRule.SYSTEM_ERROR.getMessage()))
|
||||
.getErrorRule()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.spring.common.properties;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.ConstructorBinding;
|
||||
import org.springframework.boot.context.properties.bind.DefaultValue;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@ConstructorBinding
|
||||
@ConfigurationProperties(prefix = "path")
|
||||
@RequiredArgsConstructor
|
||||
public class PathProperties {
|
||||
|
||||
private final Map<String, PathConfig> paths;
|
||||
|
||||
@Getter
|
||||
public static class PathConfig {
|
||||
private final String upload;
|
||||
private final String dowonload;
|
||||
|
||||
public PathConfig(
|
||||
@DefaultValue("") String upload,
|
||||
@DefaultValue("") String dowonload
|
||||
) {
|
||||
this.upload = upload;
|
||||
this.dowonload = dowonload;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.spring.common.util;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ProfileUtils {
|
||||
|
||||
private final Environment environment;
|
||||
|
||||
/**
|
||||
* 현재 활성화된 프로파일 목록을 반환합니다.
|
||||
*
|
||||
* @return 현재 활성화된 프로파일 배열
|
||||
*/
|
||||
public String[] getActiveProfiles() {
|
||||
return environment.getActiveProfiles();
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 프로파일이 'prod'인지 확인하는 메소드.
|
||||
*
|
||||
* @return true if the current profile is 'prod', false otherwise.
|
||||
*/
|
||||
public boolean isProdProfile() {
|
||||
return isProfileActive("prod");
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 프로파일이 'dev'인지 확인하는 메소드.
|
||||
*
|
||||
* @return true if the current profile is 'dev', false otherwise.
|
||||
*/
|
||||
public boolean isDevProfile() {
|
||||
return isProfileActive("dev");
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 프로파일이 활성화되어 있는지 확인하는 메소드.
|
||||
*
|
||||
* @param profile 확인할 프로파일 이름
|
||||
* @return true if the specified profile is active, false otherwise.
|
||||
*/
|
||||
public boolean isProfileActive(String profile) {
|
||||
return Arrays.asList(environment.getActiveProfiles()).contains(profile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 활성화된 프로파일이 여러 개일 경우, 그 중 하나라도 주어진 프로파일이 포함되어 있는지 확인하는 메소드.
|
||||
*
|
||||
* @param profiles 확인할 프로파일 이름 배열
|
||||
* @return true if any of the specified profiles are active, false otherwise.
|
||||
*/
|
||||
public boolean isAnyProfileActive(String... profiles) {
|
||||
for (String profile : profiles) {
|
||||
if (isProfileActive(profile)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 활성화된 프로파일 중 가장 우선순위가 높은 프로파일을 반환합니다.
|
||||
*
|
||||
* @return 가장 우선순위가 높은 프로파일 이름, 없으면 null
|
||||
*/
|
||||
public String getHighestPriorityProfile() {
|
||||
String[] activeProfiles = getActiveProfiles();
|
||||
return activeProfiles.length > 0 ? activeProfiles[0] : null; // 첫 번째 프로파일이 가장 우선순위가 높다고 가정
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 프로파일이 설정되어 있는지 확인하는 메소드.
|
||||
*
|
||||
* @return true if the default profile is active, false otherwise.
|
||||
*/
|
||||
public boolean isDefaultProfileActive() {
|
||||
return isProfileActive("default");
|
||||
}
|
||||
|
||||
/**
|
||||
* 주어진 프로파일이 활성화되어 있지 않은지 확인하는 메소드.
|
||||
*
|
||||
* @param profile 확인할 프로파일 이름
|
||||
* @return true if the specified profile is not active, false otherwise.
|
||||
*/
|
||||
public boolean isProfileInactive(String profile) {
|
||||
return !isProfileActive(profile);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import org.springframework.batch.core.step.tasklet.Tasklet;
|
||||
import org.springframework.batch.repeat.RepeatStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.spring.domain.email.service.EmailSendService;
|
||||
import com.spring.domain.post.repository.PostRepository;
|
||||
import com.spring.infra.batch.AbstractBatchTask;
|
||||
import com.spring.infra.batch.BatchJobInfo;
|
||||
@@ -26,10 +27,11 @@ import lombok.extern.slf4j.Slf4j;
|
||||
public class EmailSendBatch extends AbstractBatchTask {
|
||||
|
||||
private final PostRepository postRepository;
|
||||
private final EmailSendService emailSendService;
|
||||
|
||||
@Override
|
||||
protected List<Step> createSteps() {
|
||||
log.info("EmailSendBatch -> createSteps");
|
||||
// log.info("EmailSendBatch -> createSteps");
|
||||
return List.of(
|
||||
addStep("emailSendJobStep1", createTasklet()),
|
||||
addStep("emailSendJobStep2", createSendTasklet())
|
||||
@@ -40,13 +42,14 @@ public class EmailSendBatch extends AbstractBatchTask {
|
||||
protected Tasklet createTasklet() {
|
||||
log.info("EmailSendBatch -> createTasklet");
|
||||
return ((contribution, chunkContext) -> {
|
||||
postRepository.findAll();
|
||||
emailSendService.sendEmail();
|
||||
// postRepository.findAll();
|
||||
return RepeatStatus.FINISHED;
|
||||
});
|
||||
}
|
||||
|
||||
private Tasklet createSendTasklet() {
|
||||
log.info("EmailSendBatch -> createSendTasklet");
|
||||
// log.info("EmailSendBatch -> createSendTasklet");
|
||||
return ((contribution, chunkContext) -> RepeatStatus.FINISHED);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.spring.domain.email.service;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.spring.common.properties.PathProperties;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class EmailSendService {
|
||||
|
||||
private final PathProperties pathProperties;
|
||||
|
||||
public void sendEmail() {
|
||||
log.info("pathProperties: {}", pathProperties);
|
||||
log.info("paths: {}", pathProperties.getPaths());
|
||||
|
||||
String upload = pathProperties.getPaths().get("path1").getUpload();
|
||||
log.info(upload);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,31 +16,31 @@ import com.spring.infra.db.orm.jpa.SecondaryJpaConfig;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@BatchJobInfo(
|
||||
group = "${batch-info.post-batch.group}",
|
||||
jobName = "${batch-info.post-batch.job-name}",
|
||||
cronExpression = "${batch-info.post-batch.cron-expression}",
|
||||
description = "${batch-info.post-batch.description}"
|
||||
)
|
||||
@RequiredArgsConstructor
|
||||
public class PostCreateBatch extends AbstractBatchTask {
|
||||
// @Slf4j
|
||||
// @Component
|
||||
// @BatchJobInfo(
|
||||
// group = "${batch-info.post-batch.group}",
|
||||
// jobName = "${batch-info.post-batch.job-name}",
|
||||
// cronExpression = "${batch-info.post-batch.cron-expression}",
|
||||
// description = "${batch-info.post-batch.description}"
|
||||
// )
|
||||
// @RequiredArgsConstructor
|
||||
public class PostCreateBatch { //extends AbstractBatchTask {
|
||||
|
||||
private final PostMapper postMapper;
|
||||
// private final PostMapper postMapper;
|
||||
|
||||
@Autowired
|
||||
@Override
|
||||
public void setTransactionManager(@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager) {
|
||||
super.setTransactionManager(transactionManager);
|
||||
}
|
||||
// @Autowired
|
||||
// @Override
|
||||
// public void setTransactionManager(@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager) {
|
||||
// super.setTransactionManager(transactionManager);
|
||||
// }
|
||||
|
||||
@Override
|
||||
protected Tasklet createTasklet() {
|
||||
return ((contribution, chunkContext) -> {
|
||||
postMapper.save(Post.builder().title("testTitle").content("testPost").build());
|
||||
return RepeatStatus.FINISHED;
|
||||
});
|
||||
}
|
||||
// @Override
|
||||
// protected Tasklet createTasklet() {
|
||||
// return ((contribution, chunkContext) -> {
|
||||
// postMapper.save(Post.builder().title("testTitle").content("testPost").build());
|
||||
// return RepeatStatus.FINISHED;
|
||||
// });
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
@@ -36,106 +36,106 @@ import com.spring.infra.db.orm.jpa.SecondaryJpaConfig;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@BatchJobInfo(
|
||||
group = "${batch-info.post-create-batch.group}",
|
||||
jobName = "${batch-info.post-create-batch.job-name}",
|
||||
cronExpression = "${batch-info.post-create-batch.cron-expression}",
|
||||
description = "${batch-info.post-create-batch.description}"
|
||||
)
|
||||
@RequiredArgsConstructor
|
||||
public class PostCreateBatchChunk extends AbstractBatchChunk {
|
||||
// @Slf4j
|
||||
// @Component
|
||||
// @BatchJobInfo(
|
||||
// group = "${batch-info.post-create-batch.group}",
|
||||
// jobName = "${batch-info.post-create-batch.job-name}",
|
||||
// cronExpression = "${batch-info.post-create-batch.cron-expression}",
|
||||
// description = "${batch-info.post-create-batch.description}"
|
||||
// )
|
||||
// @RequiredArgsConstructor
|
||||
public class PostCreateBatchChunk { //extends AbstractBatchChunk {
|
||||
|
||||
@Autowired
|
||||
@Override
|
||||
public void setTransactionManager(@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager) {
|
||||
super.setTransactionManager(transactionManager);
|
||||
}
|
||||
// @Autowired
|
||||
// @Override
|
||||
// public void setTransactionManager(@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager) {
|
||||
// super.setTransactionManager(transactionManager);
|
||||
// }
|
||||
|
||||
@Qualifier(SecondaryJpaConfig.ENTITY_MANAGER_FACTORY)
|
||||
private final EntityManagerFactory entityManagerFactory;
|
||||
// @Qualifier(SecondaryJpaConfig.ENTITY_MANAGER_FACTORY)
|
||||
// private final EntityManagerFactory entityManagerFactory;
|
||||
|
||||
private final PostRepository postRepository;
|
||||
private final PostBackUpRepository postBackUpRepository;
|
||||
// private final PostRepository postRepository;
|
||||
// private final PostBackUpRepository postBackUpRepository;
|
||||
|
||||
private List<Post> list = new ArrayList<>();
|
||||
// private List<Post> list = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public Job createJob() {
|
||||
return new JobBuilder(batchJobInfoData.getJobName())
|
||||
.repository(jobRepository)
|
||||
.incrementer(new RunIdIncrementer())
|
||||
.start(readListStep())
|
||||
.next(decider())
|
||||
.from(decider()).on("PROCESS").to(processStep())
|
||||
.from(decider()).on("TERMINATE").to(terminateStep())
|
||||
.end()
|
||||
.build();
|
||||
}
|
||||
// @Override
|
||||
// public Job createJob() {
|
||||
// return new JobBuilder(batchJobInfoData.getJobName())
|
||||
// .repository(jobRepository)
|
||||
// .incrementer(new RunIdIncrementer())
|
||||
// .start(readListStep())
|
||||
// .next(decider())
|
||||
// .from(decider()).on("PROCESS").to(processStep())
|
||||
// .from(decider()).on("TERMINATE").to(terminateStep())
|
||||
// .end()
|
||||
// .build();
|
||||
// }
|
||||
|
||||
private JobExecutionDecider decider() {
|
||||
return (JobExecution jobExecution, StepExecution stepExecution) ->
|
||||
!list.isEmpty() ? new FlowExecutionStatus("PROCESS") : new FlowExecutionStatus("TERMINATE");
|
||||
}
|
||||
// private JobExecutionDecider decider() {
|
||||
// return (JobExecution jobExecution, StepExecution stepExecution) ->
|
||||
// !list.isEmpty() ? new FlowExecutionStatus("PROCESS") : new FlowExecutionStatus("TERMINATE");
|
||||
// }
|
||||
|
||||
private Step readListStep() {
|
||||
return new StepBuilder("readListStep")
|
||||
.repository(jobRepository)
|
||||
.transactionManager(transactionManager)
|
||||
.tasklet(readListTasklet())
|
||||
.build();
|
||||
}
|
||||
// private Step readListStep() {
|
||||
// return new StepBuilder("readListStep")
|
||||
// .repository(jobRepository)
|
||||
// .transactionManager(transactionManager)
|
||||
// .tasklet(readListTasklet())
|
||||
// .build();
|
||||
// }
|
||||
|
||||
private Tasklet readListTasklet() {
|
||||
return (contribution, chunkContext) -> {
|
||||
list = postRepository.findAll();
|
||||
return RepeatStatus.FINISHED;
|
||||
};
|
||||
}
|
||||
// private Tasklet readListTasklet() {
|
||||
// return (contribution, chunkContext) -> {
|
||||
// list = postRepository.findAll();
|
||||
// return RepeatStatus.FINISHED;
|
||||
// };
|
||||
// }
|
||||
|
||||
private Step processStep() {
|
||||
return new StepBuilder("processStep")
|
||||
.repository(jobRepository)
|
||||
.transactionManager(transactionManager)
|
||||
.<Post, PostBackUp>chunk(5)
|
||||
.reader(testReader())
|
||||
.processor(testProcessor())
|
||||
.writer(testWriter())
|
||||
.build();
|
||||
}
|
||||
// private Step processStep() {
|
||||
// return new StepBuilder("processStep")
|
||||
// .repository(jobRepository)
|
||||
// .transactionManager(transactionManager)
|
||||
// .<Post, PostBackUp>chunk(5)
|
||||
// .reader(testReader())
|
||||
// .processor(testProcessor())
|
||||
// .writer(testWriter())
|
||||
// .build();
|
||||
// }
|
||||
|
||||
private JpaPagingItemReader<Post> testReader() {
|
||||
return new JpaPagingItemReaderBuilder<Post>()
|
||||
.name("testReader")
|
||||
.entityManagerFactory(entityManagerFactory)
|
||||
.pageSize(5)
|
||||
.queryString("select p from Post p")
|
||||
.build();
|
||||
}
|
||||
// private JpaPagingItemReader<Post> testReader() {
|
||||
// return new JpaPagingItemReaderBuilder<Post>()
|
||||
// .name("testReader")
|
||||
// .entityManagerFactory(entityManagerFactory)
|
||||
// .pageSize(5)
|
||||
// .queryString("select p from Post p")
|
||||
// .build();
|
||||
// }
|
||||
|
||||
private ItemProcessor<Post, PostBackUp> testProcessor() {
|
||||
return post ->
|
||||
PostBackUp.builder().postId(post.getPostId()).content(post.getContent()).title(post.getTitle()).build();
|
||||
}
|
||||
// private ItemProcessor<Post, PostBackUp> testProcessor() {
|
||||
// return post ->
|
||||
// PostBackUp.builder().postId(post.getPostId()).content(post.getContent()).title(post.getTitle()).build();
|
||||
// }
|
||||
|
||||
private ItemWriter<PostBackUp> testWriter() {
|
||||
return postBackUpRepository::saveAll;
|
||||
}
|
||||
// private ItemWriter<PostBackUp> testWriter() {
|
||||
// return postBackUpRepository::saveAll;
|
||||
// }
|
||||
|
||||
private Step terminateStep() {
|
||||
return new StepBuilder("terminateStep")
|
||||
.repository(jobRepository)
|
||||
.transactionManager(transactionManager)
|
||||
.tasklet(terminateTasklet())
|
||||
.build();
|
||||
}
|
||||
// private Step terminateStep() {
|
||||
// return new StepBuilder("terminateStep")
|
||||
// .repository(jobRepository)
|
||||
// .transactionManager(transactionManager)
|
||||
// .tasklet(terminateTasklet())
|
||||
// .build();
|
||||
// }
|
||||
|
||||
private Tasklet terminateTasklet() {
|
||||
return (contribution, chunkContext) -> {
|
||||
log.error("List Read Error : List is null");
|
||||
return RepeatStatus.FINISHED;
|
||||
};
|
||||
}
|
||||
// private Tasklet terminateTasklet() {
|
||||
// return (contribution, chunkContext) -> {
|
||||
// log.error("List Read Error : List is null");
|
||||
// return RepeatStatus.FINISHED;
|
||||
// };
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
@@ -79,6 +79,8 @@ public abstract class AbstractBatchTask implements AbstractBatch, ApplicationCon
|
||||
*/
|
||||
private PlatformTransactionManager transactionManager;
|
||||
|
||||
private BatchExceptionListener batchExceptionListener;
|
||||
|
||||
/**
|
||||
* 기본 생성자입니다.
|
||||
*
|
||||
@@ -163,6 +165,11 @@ public abstract class AbstractBatchTask implements AbstractBatch, ApplicationCon
|
||||
this.transactionManager = transactionManager;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setBatchExceptionListener(BatchExceptionListener batchExceptionListener) {
|
||||
this.batchExceptionListener = batchExceptionListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치 작업을 생성합니다.
|
||||
*
|
||||
@@ -180,6 +187,7 @@ public abstract class AbstractBatchTask implements AbstractBatch, ApplicationCon
|
||||
var jobBuilder = new JobBuilder(batchJobInfoData.getJobName())
|
||||
.incrementer(new RunIdIncrementer())
|
||||
.repository(jobRepository)
|
||||
.listener(batchExceptionListener)
|
||||
.start(steps.get(0));
|
||||
for (int i = 1; i < steps.size(); i++) {
|
||||
jobBuilder = jobBuilder.next(steps.get(i));
|
||||
@@ -213,6 +221,7 @@ public abstract class AbstractBatchTask implements AbstractBatch, ApplicationCon
|
||||
return new StepBuilder(stepName)
|
||||
.repository(jobRepository)
|
||||
.transactionManager(transactionManager)
|
||||
.listener(batchExceptionListener)
|
||||
.tasklet(tasklet)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.spring.infra.batch;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.batch.core.BatchStatus;
|
||||
import org.springframework.batch.core.ExitStatus;
|
||||
import org.springframework.batch.core.JobExecution;
|
||||
import org.springframework.batch.core.JobExecutionListener;
|
||||
import org.springframework.batch.core.StepExecution;
|
||||
import org.springframework.batch.core.StepExecutionListener;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class BatchExceptionListener implements JobExecutionListener, StepExecutionListener {
|
||||
|
||||
@Override
|
||||
public void beforeJob(@NonNull JobExecution jobExecution) {
|
||||
log.info("Job 시작: {}", jobExecution.getJobInstance().getJobName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterJob(@NonNull JobExecution jobExecution) {
|
||||
if (jobExecution.getStatus() == BatchStatus.FAILED) {
|
||||
handleJobFailure(jobExecution);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeStep(@NonNull StepExecution stepExecution) {
|
||||
log.info("Step 시작: {}", stepExecution.getStepName());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ExitStatus afterStep(@NonNull StepExecution stepExecution) {
|
||||
if (!stepExecution.getFailureExceptions().isEmpty()) {
|
||||
Throwable ex = stepExecution.getFailureExceptions().get(0);
|
||||
|
||||
if (ex instanceof DataAccessException) {
|
||||
// SQL 예외 처리
|
||||
DataAccessException sqlEx = (DataAccessException) ex;
|
||||
log.error("SQL 오류 발생: {}", sqlEx.getMostSpecificCause().getMessage());
|
||||
return ExitStatus.FAILED;
|
||||
} else {
|
||||
// 기타 예외 처리
|
||||
log.error("예외 발생: ", ex);
|
||||
return ExitStatus.FAILED;
|
||||
}
|
||||
}
|
||||
return stepExecution.getExitStatus();
|
||||
}
|
||||
|
||||
private void handleJobFailure(JobExecution jobExecution) {
|
||||
List<Throwable> exceptions = jobExecution.getAllFailureExceptions();
|
||||
for (Throwable ex : exceptions) {
|
||||
if (ex instanceof DataAccessException) {
|
||||
handleSqlException((DataAccessException) ex, jobExecution);
|
||||
} else {
|
||||
handleGeneralException(ex, jobExecution);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSqlException(DataAccessException ex, JobExecution jobExecution) {
|
||||
log.error("Job {} 실행 중 SQL 오류 발생: {}",
|
||||
jobExecution.getJobInstance().getJobName(),
|
||||
ex.getMostSpecificCause().getMessage());
|
||||
}
|
||||
|
||||
private void handleGeneralException(Throwable ex, JobExecution jobExecution) {
|
||||
log.error("Job {} 실행 중 오류 발생: {}",
|
||||
jobExecution.getJobInstance().getJobName(),
|
||||
ex.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.spring.infra.feign.client;
|
||||
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.cloud.openfeign.SpringQueryMap;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
|
||||
import com.spring.infra.feign.config.CommonFeignConfig;
|
||||
import com.spring.infra.feign.dto.TestRequest;
|
||||
|
||||
@FeignClient(name = "client1", url = "${feign.clients.client1.url}", configuration = CommonFeignConfig.class)
|
||||
public interface Client1FeignClient {
|
||||
|
||||
@PostMapping(value = "/test", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
String getItemById(@SpringQueryMap TestRequest request);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.spring.infra.feign.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.cloud.openfeign.FeignFormatterRegistrar;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import feign.Client;
|
||||
import feign.Logger;
|
||||
import feign.Request;
|
||||
import feign.RequestInterceptor;
|
||||
import feign.Response;
|
||||
import feign.RetryableException;
|
||||
import feign.Retryer;
|
||||
import feign.codec.ErrorDecoder;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.RequestBody;
|
||||
|
||||
/**
|
||||
* Feign 클라이언트에 대한 공통 설정을 제공하는 설정 클래스입니다.
|
||||
* 이 클래스는 기본 Feign 빌더 설정과 클라이언트별 커스텀 설정을 제공합니다.
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class CommonFeignConfig {
|
||||
|
||||
private final FeignClientProperties properties;
|
||||
|
||||
@Bean
|
||||
Client feignClient() {
|
||||
return new Client() {
|
||||
@Override
|
||||
public Response execute(Request request, Request.Options options) throws IOException {
|
||||
String clientName = request.requestTemplate().feignTarget().name();
|
||||
FeignClientProperties.ClientConfig config = properties.getClients().get(clientName);
|
||||
|
||||
OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder()
|
||||
.connectTimeout(config.getConnectTimeout(), TimeUnit.MILLISECONDS)
|
||||
.readTimeout(config.getReadTimeout(), TimeUnit.MILLISECONDS);
|
||||
|
||||
if (config.isUseProxy()) {
|
||||
clientBuilder.proxy(new Proxy(
|
||||
Proxy.Type.HTTP,
|
||||
new InetSocketAddress(config.getProxyHost(), config.getProxyPort())
|
||||
));
|
||||
}
|
||||
|
||||
OkHttpClient okHttpClient = clientBuilder.build();
|
||||
// OkHttp 요청 생성
|
||||
okhttp3.Request.Builder okHttpRequestBuilder = new okhttp3.Request.Builder()
|
||||
.url(request.url())
|
||||
.method(request.httpMethod().name(), getRequestBody(request)); // 요청 본문 설정
|
||||
|
||||
// Feign 요청의 헤더를 OkHttp 요청에 추가
|
||||
request.headers().forEach((key, values) -> {
|
||||
for (String value : values) {
|
||||
okHttpRequestBuilder.addHeader(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
okhttp3.Request okHttpRequest = okHttpRequestBuilder.build();
|
||||
|
||||
try (okhttp3.Response okHttpResponse = okHttpClient.newCall(okHttpRequest).execute()) {
|
||||
return Response.builder()
|
||||
.status(okHttpResponse.code())
|
||||
.reason(okHttpResponse.message())
|
||||
.headers(convertHeaders(okHttpResponse.headers()))
|
||||
.body(okHttpResponse.body().bytes())
|
||||
.request(request)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Map<String, Collection<String>> convertHeaders(okhttp3.Headers headers) {
|
||||
Map<String, Collection<String>> convertedHeaders = new HashMap<>();
|
||||
for (String name : headers.names()) {
|
||||
convertedHeaders.put(name, headers.values(name));
|
||||
}
|
||||
return convertedHeaders;
|
||||
}
|
||||
|
||||
private RequestBody getRequestBody(Request request) {
|
||||
String contentType = MediaType.APPLICATION_JSON_VALUE;
|
||||
|
||||
if (request.requestTemplate().headers().containsKey("Content-Type")) {
|
||||
contentType = request.requestTemplate().headers().get("Content-Type").iterator().next();
|
||||
}
|
||||
|
||||
if (request.body() != null) {
|
||||
return RequestBody.create(request.body(), okhttp3.MediaType.parse(contentType));
|
||||
} else {
|
||||
return RequestBody.create("", okhttp3.MediaType.parse(contentType));
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
RequestInterceptor headerInterceptor() {
|
||||
return requestTemplate -> {
|
||||
String clientName = requestTemplate.feignTarget().name();
|
||||
if (properties.getClients().containsKey(clientName)) {
|
||||
var config = properties.getClients().get(clientName);
|
||||
if (!config.getHeaders().isEmpty()) {
|
||||
config.getHeaders().forEach(requestTemplate::header);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
Retryer retryer() {
|
||||
// 0.1초의 간격으로 시작해 최대 3초의 간격으로 점점 증가하며, 최대5번 재시도한다.
|
||||
return new Retryer.Default(100L, TimeUnit.SECONDS.toMillis(3L), 5);
|
||||
}
|
||||
|
||||
@Bean
|
||||
Logger.Level feignLoggerLevel() {
|
||||
return Logger.Level.FULL;
|
||||
}
|
||||
|
||||
@Bean
|
||||
FeignFormatterRegistrar dateTimeFormatterRegistrar() {
|
||||
return registry -> {
|
||||
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
|
||||
registrar.setUseIsoFormat(true);
|
||||
registrar.registerFormatters(registry);
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
ErrorDecoder errorDecoder() {
|
||||
return (methodKey, response) ->
|
||||
(response.status() >= 500 && response.status() <= 504)
|
||||
? new RetryableException(
|
||||
response.status(),
|
||||
String.format("Server error %d: %s, retrying...", response.status(), response.reason()),
|
||||
response.request().httpMethod(),
|
||||
null,
|
||||
response.request()
|
||||
)
|
||||
: new Exception(String.format("Error %d: %s", response.status(), response.reason()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.spring.infra.feign.config;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.ConstructorBinding;
|
||||
import org.springframework.boot.context.properties.bind.DefaultValue;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@ConstructorBinding
|
||||
@ConfigurationProperties(prefix = "feign")
|
||||
@RequiredArgsConstructor
|
||||
public class FeignClientProperties {
|
||||
|
||||
private final Map<String, ClientConfig> clients;
|
||||
|
||||
@Getter
|
||||
public static class ClientConfig {
|
||||
private final String url;
|
||||
private final int connectTimeout;
|
||||
private final int readTimeout;
|
||||
private final String proxyHost;
|
||||
private final int proxyPort;
|
||||
private final boolean useProxy;
|
||||
private final Map<String, String> headers;
|
||||
|
||||
public ClientConfig(
|
||||
String url,
|
||||
@DefaultValue("5000") int connectTimeout,
|
||||
@DefaultValue("5000") int readTimeout,
|
||||
@DefaultValue("") String proxyHost,
|
||||
@DefaultValue("0") int proxyPort,
|
||||
@DefaultValue("false") boolean useProxy,
|
||||
Map<String, String> headers
|
||||
) {
|
||||
this.url = url;
|
||||
this.connectTimeout = connectTimeout;
|
||||
this.readTimeout = readTimeout;
|
||||
this.proxyHost = proxyHost;
|
||||
this.proxyPort = proxyPort;
|
||||
this.useProxy = useProxy;
|
||||
this.headers = headers != null ? headers : new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.spring.infra.feign.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class TestRequest {
|
||||
private String id;
|
||||
private String name;
|
||||
}
|
||||
@@ -94,7 +94,7 @@ public class QuartzConfig {
|
||||
factory.setDataSource(dataSource);
|
||||
factory.setTransactionManager(transactionManager);
|
||||
factory.setJobFactory(jobFactory);
|
||||
factory.setAutoStartup(false);
|
||||
factory.setAutoStartup(true);
|
||||
factory.setWaitForJobsToCompleteOnShutdown(true);
|
||||
return factory;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
package com.spring.infra.quartz;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.Scheduler;
|
||||
import org.quartz.SchedulerException;
|
||||
import org.quartz.Trigger;
|
||||
import org.quartz.TriggerKey;
|
||||
import org.quartz.impl.matchers.GroupMatcher;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.ContextClosedEvent;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Quartz 스케줄러의 안전한 종료를 관리하는 리스너 클래스
|
||||
*
|
||||
* <p>이 클래스는 Spring 애플리케이션 컨텍스트가 종료될 때 Quartz 스케줄러의 모든 작업을
|
||||
* 안전하게 종료하고 정리하는 역할을 합니다. 실행 중인 작업이 정상적으로 완료될 때까지
|
||||
* 대기하며, 모든 트리거를 정리합니다.</p>
|
||||
*
|
||||
* <p>주요 기능:</p>
|
||||
* <ul>
|
||||
* <li>실행 중인 작업의 안전한 종료 처리</li>
|
||||
* <li>트리거 상태 모니터링 및 정리</li>
|
||||
* <li>스케줄러 리소스의 정상적인 해제</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author [작성자 이름]
|
||||
* @version 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class QuartzShutdownListener implements ApplicationListener<ContextClosedEvent> {
|
||||
|
||||
/** 작업 완료 대기 최대 시간 (밀리초) */
|
||||
private static final long SHUTDOWN_WAIT_TIME = 30000L; // 30초
|
||||
|
||||
/** Quartz 스케줄러 인스턴스 */
|
||||
private final Scheduler scheduler;
|
||||
|
||||
|
||||
/**
|
||||
* Spring 컨텍스트 종료 이벤트 발생 시 실행되는 메서드
|
||||
*
|
||||
* @param event 컨텍스트 종료 이벤트
|
||||
*/
|
||||
@Override
|
||||
public void onApplicationEvent(@NonNull ContextClosedEvent event) {
|
||||
log.info("Quartz 셧다운 프로세스 시작...");
|
||||
shutdownQuartzGracefully();
|
||||
}
|
||||
|
||||
/**
|
||||
* Quartz 스케줄러의 안전한 종료를 처리하는 메서드
|
||||
*
|
||||
* <p>다음의 단계를 순차적으로 실행합니다:</p>
|
||||
* <ol>
|
||||
* <li>새로운 작업 스케줄링 중지</li>
|
||||
* <li>현재 실행 중인 작업 목록 확인</li>
|
||||
* <li>실행 중인 작업의 상태 로깅</li>
|
||||
* <li>작업 완료 대기</li>
|
||||
* <li>트리거 정리</li>
|
||||
* <li>스케줄러 종료</li>
|
||||
* </ol>
|
||||
*/
|
||||
private void shutdownQuartzGracefully() {
|
||||
try {
|
||||
// 1. 새로운 작업 스케줄링 중지
|
||||
scheduler.standby();
|
||||
log.info("Scheduler를 대기 모드로 전환했습니다.");
|
||||
|
||||
// 2. 현재 실행 중인 작업 목록 확인
|
||||
List<JobExecutionContext> currentJobs = scheduler.getCurrentlyExecutingJobs();
|
||||
if (!currentJobs.isEmpty()) {
|
||||
log.info("현재 실행 중인 작업 수: {}", currentJobs.size());
|
||||
|
||||
// 3. 실행 중인 작업의 정보 로깅
|
||||
logRunningJobs(currentJobs);
|
||||
|
||||
// 4. 실행 중인 작업이 완료될 때까지 대기
|
||||
waitForJobCompletion(currentJobs);
|
||||
}
|
||||
|
||||
// 5. 모든 트리거 상태 확인 및 정리
|
||||
cleanupTriggers();
|
||||
|
||||
// 6. 스케줄러 종료
|
||||
performSchedulerShutdown();
|
||||
|
||||
} catch (SchedulerException e) {
|
||||
log.error("Quartz 종료 중 오류 발생", e);
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (InterruptedException e) {
|
||||
log.error("Quartz 종료 대기 중 인터럽트 발생", e);
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 실행 중인 작업들의 상세 정보를 로깅
|
||||
*
|
||||
* @param currentJobs 현재 실행 중인 작업 목록
|
||||
*/
|
||||
private void logRunningJobs(List<JobExecutionContext> currentJobs) {
|
||||
for (JobExecutionContext job : currentJobs) {
|
||||
log.info("실행 중인 작업: {}, 트리거: {}, 시작 시간: {}",
|
||||
job.getJobDetail().getKey(),
|
||||
job.getTrigger().getKey(),
|
||||
job.getFireTime()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 실행 중인 작업이 완료될 때까지 대기
|
||||
*
|
||||
* @param currentJobs 현재 실행 중인 작업 목록
|
||||
* @throws InterruptedException 대기 중 인터럽트 발생 시
|
||||
* @throws SchedulerException 스케줄러 관련 오류 발생 시
|
||||
*/
|
||||
private void waitForJobCompletion(List<JobExecutionContext> currentJobs)
|
||||
throws InterruptedException, SchedulerException {
|
||||
long startTime = System.currentTimeMillis();
|
||||
while (!currentJobs.isEmpty() &&
|
||||
(System.currentTimeMillis() - startTime) < SHUTDOWN_WAIT_TIME) {
|
||||
log.info("실행 중인 작업이 완료될 때까지 대기 중...");
|
||||
Thread.sleep(1000);
|
||||
currentJobs = scheduler.getCurrentlyExecutingJobs();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 트리거를 정리
|
||||
*
|
||||
* @throws SchedulerException 스케줄러 관련 오류 발생 시
|
||||
*/
|
||||
private void cleanupTriggers() throws SchedulerException {
|
||||
List<String> groups = scheduler.getTriggerGroupNames();
|
||||
for (String group : groups) {
|
||||
Set<TriggerKey> triggerKeys = scheduler.getTriggerKeys(GroupMatcher.groupEquals(group));
|
||||
for (TriggerKey triggerKey : triggerKeys) {
|
||||
try {
|
||||
Trigger trigger = scheduler.getTrigger(triggerKey);
|
||||
if (trigger != null) {
|
||||
scheduler.unscheduleJob(triggerKey);
|
||||
log.info("트리거 제거됨: {}", triggerKey);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("트리거 제거 중 오류 발생: {}", triggerKey, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 스케줄러 종료 수행
|
||||
*
|
||||
* @throws SchedulerException 스케줄러 관련 오류 발생 시
|
||||
*/
|
||||
private void performSchedulerShutdown() throws SchedulerException {
|
||||
scheduler.clear();
|
||||
scheduler.shutdown(true); // true = 실행 중인 작업이 완료될 때까지 대기
|
||||
log.info("Quartz 스케줄러가 정상적으로 종료되었습니다.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 애플리케이션 컨텍스트 종료 전 실행되는 정리 메서드
|
||||
*/
|
||||
@PreDestroy
|
||||
public void preDestroy() {
|
||||
try {
|
||||
if (!scheduler.isShutdown()) {
|
||||
log.info("ApplicationContext 종료 전 Quartz 정리 작업 수행");
|
||||
shutdownQuartzGracefully();
|
||||
}
|
||||
} catch (SchedulerException e) {
|
||||
log.error("PreDestroy 단계에서 Quartz 종료 중 오류 발생", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -102,7 +102,7 @@ batch-info:
|
||||
email-send-batch:
|
||||
group: "EMAIL"
|
||||
job-name: "emailSendJob"
|
||||
cron-expression: "*/20 * * * * ?"
|
||||
cron-expression: "*/10 * * * * ?"
|
||||
description: "이메일배치작업"
|
||||
post-batch:
|
||||
group: "POST"
|
||||
@@ -113,7 +113,36 @@ batch-info:
|
||||
group: "POST"
|
||||
job-name: "postCreateJob"
|
||||
cron-expression: "0/30 * * * * ?"
|
||||
description: "테스트배치작업"
|
||||
description: "테스트배치작업"
|
||||
|
||||
feign:
|
||||
okhttp:
|
||||
enabled: true
|
||||
client:
|
||||
config:
|
||||
default:
|
||||
connectTimeout: 5000
|
||||
readTimeout: 5000
|
||||
loggerLevel: full
|
||||
compression:
|
||||
request:
|
||||
enabled: true
|
||||
response:
|
||||
enabled: true
|
||||
|
||||
clients:
|
||||
client1:
|
||||
url: http://localhost:8082
|
||||
connectTimeout: 5000
|
||||
readTimeout: 5000
|
||||
useProxy: true
|
||||
proxyHost: http://localhost:8083
|
||||
proxyPort: 8083
|
||||
client2:
|
||||
url: http://api1.example.com
|
||||
headers:
|
||||
Client1-Specific-Header: Value1
|
||||
Another-Client1-Header: AnotherValue1
|
||||
|
||||
jwt:
|
||||
access-token:
|
||||
@@ -123,6 +152,14 @@ jwt:
|
||||
secret: bnhjdXMyLjAtcGxhdGZvcm0tcHJvamVjdC13aXRoLXNwcmluZy1ib290bnhjdF9zdHJvbmdfY29tcGxleF9zdHJvbmc=
|
||||
expiration: 10080
|
||||
|
||||
path:
|
||||
paths:
|
||||
path1:
|
||||
upload: /path1/upload
|
||||
path2:
|
||||
upload: /path2/upload
|
||||
dowonload: /path2/dowonload
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
|
||||
Reference in New Issue
Block a user