diff --git a/pom.xml b/pom.xml
index eac3e00..deb26e8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,7 +18,7 @@
+ * description : Api 응답 + * TODO :: 프로젝트별 json 결과에서 제외하려면 @JsonIgnore 사용 + * packageName : kr.xit.core.model + * fileName : ApiResponseDTO + * author : julim + * date : 2023-04-28 + * ====================================================================== + * 변경일 변경자 변경 내용 + * ---------------------------------------------------------------------- + * 2023-04-28 julim 최초 생성 + * + *+ */ +@Schema(name = "ApiResponseDTO", description = "Restful API 결과") +@Data +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@JsonRootName("result") +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ApiResponseDTO
+ * description : + * + * packageName : kr.xit.core.model + * fileName : IApiResponse + * author : limju + * date : 2023-05-31 + * ====================================================================== + * 변경일 변경자 변경 내용 + * ---------------------------------------------------------------------- + * 2023-05-31 limju 최초 생성 + * + *+ */ +public interface IApiResponse { +} diff --git a/src/main/java/cokr/xit/ens/core/config/support/CustomJacksonConfig.java b/src/main/java/cokr/xit/ens/core/config/support/CustomJacksonConfig.java new file mode 100644 index 0000000..5c832e9 --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/config/support/CustomJacksonConfig.java @@ -0,0 +1,75 @@ +package cokr.xit.ens.core.config.support; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.cfg.CoercionAction; +import com.fasterxml.jackson.databind.cfg.CoercionInputShape; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import cokr.xit.ens.core.utils.CoreSpringUtils; +import cokr.xit.ens.core.utils.JsonUtils; + +/** + *
+ * description : Jackson(ObjectMapper) 설정 재정의 + * 모든 ObjectMapper 사용시 이 설정이 적용되도록 해야 한다. + * - Cannot coerce empty String("") to ... + * value(but could if coercion was enabled using `CoercionConfig`) 에러 방어 + * packageName : kr.xit.core.spring.config.support + * fileName : CustomJacksonConfig + * author : julim + * date : 2023-09-13 + * ====================================================================== + * 변경일 변경자 변경 내용 + * ---------------------------------------------------------------------- + * 2023-09-13 julim 최초 생성 + * + *+ * @see CoreSpringUtils#getObjectMapper() + * @see JsonUtils + * @see WebClientConfig + */ +@Configuration +public class CustomJacksonConfig { + + /** + *
+ * 기본 설정에 override + * Cannot coerce empty String("") to ... + * value(but could if coercion was enabled using `CoercionConfig`) 에러 방어 + *+ * + * @return + */ + @Bean + @Primary + public ObjectMapper objectMapper() { + ObjectMapper om = new ObjectMapper() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + //.setSerializationInclusion(Include.NON_NULL) + //.setSerializationInclusion(Include.NON_EMPTY) + .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, false) + .registerModule(new JavaTimeModule()); + + om.coercionConfigDefaults() + .setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull); +// om.coercionConfigFor(LogicalType.Enum) +// .setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull); +// om.coercionConfigFor(LogicalType.POJO) +// .setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull); + + return om; + } +} diff --git a/src/main/java/cokr/xit/ens/core/config/support/WebClientConfig.java b/src/main/java/cokr/xit/ens/core/config/support/WebClientConfig.java new file mode 100644 index 0000000..c159d17 --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/config/support/WebClientConfig.java @@ -0,0 +1,245 @@ +package cokr.xit.ens.core.config.support; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLException; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeStrategies; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.DefaultUriBuilderFactory; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import cokr.xit.ens.core.exception.BizRuntimeException; +import cokr.xit.ens.core.exception.ClientErrorException; +import cokr.xit.ens.core.exception.ServerErrorException; +import io.netty.channel.ChannelOption; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.timeout.ReadTimeoutHandler; +import io.netty.handler.timeout.WriteTimeoutHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; +import reactor.netty.transport.logging.AdvancedByteBufFormat; + +/** + *
+ * description : WebClient configuration + * Spring WebFlux 기반 HTTP Client Config + * logging : - ExchangeFilterFunction 구현 처리 가능(requestFilter, responseFilter) + * - logging.level.reactor.netty.http.client: DEBUG|ERROR + * + * packageName : kr.xit.core.spring.config.support + * fileName : WebClientConfig + * author : julim + * date : 2023-09-06 + * ====================================================================== + * 변경일 변경자 변경 내용 + * ---------------------------------------------------------------------- + * 2023-09-06 julim 최초 생성 + * + *+ */ +@Slf4j +@RequiredArgsConstructor +@Configuration +public class WebClientConfig { + + @Value("${app.contract.connection.timeout:5000}") + private int connectTimeout; + @Value("${app.contract.connection.readTimeout:5000}") + private int readTimeout; + + private final ObjectMapper objectMapper; + DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(); + + /** + * setEncodingMode : GET 요청의 파라미터 셋팅을 하기 위한 URI 템플릿의 인코딩을 위한 설정 + * @return + */ + @Bean + public WebClient webClient() { + factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY); + + return WebClient.builder() + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .uriBuilderFactory(factory) + .clientConnector(new ReactorClientHttpConnector(defaultHttpClient())) + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)) + .exchangeStrategies(defaultExchangeStrategies()) + .filters(exchangeFilterFunctions -> { + //exchangeFilterFunctions.add(requestFilter()); + exchangeFilterFunctions.add(responseFilter()); + }) + .build(); + } + + /** + *
+ * Http client 생성 + * - 요청 / 응답 debugging을 위해 wiretap 설정 - AdvancedByteBufFormat.TEXTUAL + * @return HttpClient + *+ */ + @Bean + public HttpClient defaultHttpClient() { + try { + // SSL check bypass + SslContext sslContext = SslContextBuilder + .forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .build(); + + return HttpClient.create(connectionProvider()) + .secure(t -> t.sslContext(sslContext)) + .wiretap(this.getClass().getCanonicalName(), LogLevel.DEBUG, + AdvancedByteBufFormat.TEXTUAL) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout) + .responseTimeout(Duration.ofMillis(this.connectTimeout)) + .doOnConnected(conn -> + conn.addHandlerLast(new ReadTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS)) + .addHandlerLast( + new WriteTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS))); + }catch(SSLException se){ + throw BizRuntimeException.create(se.getMessage()); + } + } + + /** + * maxConnections : connection pool의 갯수 + * pendingAcquireTimeout : 커넥션 풀에서 커넥션을 얻기 위해 기다리는 최대 시간 + * pendingAcquireMaxCount : 커넥션 풀에서 커넥션을 가져오는 시도 횟수 (-1: no limit) + * maxIdleTime : 커넥션 풀에서 idle 상태의 커넥션을 유지하는 시간 + * @return ConnectionProvider + */ + @Bean + public ConnectionProvider connectionProvider() { + return ConnectionProvider.builder("http-pool") + .maxConnections(100) + .pendingAcquireTimeout(Duration.ofMillis(0)) + .pendingAcquireMaxCount(-1) + .maxIdleTime(Duration.ofMillis(2000L)) + .build(); + } + + /** + *
+ * 1. 256KB 보다 큰 HTTP 메시지를 처리 시도 → DataBufferLimitException 에러 발생 방어 + * 2. messageWriters를 통한 logging(setEnableLoggingRequestDetails(true) + * -> org.springframework.web.reactive.function.client.ExchangeFunctions: DEBUG 하여 활성 + * -> defaultHttpClient()의 wiretap 사용으로 비활성 + *+ * @return ExchangeStrategies + */ + @Bean + public ExchangeStrategies defaultExchangeStrategies() { + // 256KB 보다 큰 HTTP 메시지를 처리 시도 → DataBufferLimitException 에러 발생 방어 + return ExchangeStrategies.builder() + .codecs(config -> { + config.defaultCodecs().maxInMemorySize(2 * 1024 * 1024); + config.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON)); + config.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON)); + }) + .build(); + +// es.messageWriters() +// .stream() +// .filter(LoggingCodecSupport.class::isInstance) +// .forEach(writer -> ((LoggingCodecSupport)writer).setEnableLoggingRequestDetails(true)); + } + + private ExchangeFilterFunction requestFilter() { + return ExchangeFilterFunction.ofRequestProcessor(cr -> { + if (log.isDebugEnabled()) { + StringBuilder sb = new StringBuilder("\n>>>>>>>>>> WebClient Http Request <<<<<<<<<<<<<\n"); + sb.append(logMethodAndUrl(cr)); + sb.append(logHeaders(cr)); + sb.append("-------------------------------------------------------"); + log.debug(sb.toString()); + } + return Mono.just(cr); + }); + } + + /** + * reponse logging && error Handling + * @return ExchangeFilterFunction + */ + private ExchangeFilterFunction responseFilter() { + return ExchangeFilterFunction.ofResponseProcessor(cr -> { + + HttpStatus status = cr.statusCode(); + + if(cr.statusCode().is4xxClientError()) { + return cr.bodyToMono(String.class) + .flatMap(errorBody -> Mono.error(new ClientErrorException(status, errorBody))); + + } else if(cr.statusCode().is5xxServerError()) { + return cr.bodyToMono(String.class) + .flatMap(errorBody -> Mono.error(new ServerErrorException(status, errorBody))); + } + +// if(log.isDebugEnabled()) { +// StringBuilder sb = new StringBuilder( +// "\n>>>>>>>>>> WebClient Http Response <<<<<<<<<<<<<\n"); +// sb.append(logStatus(cr)); +// sb.append(logHeaders(cr)); +// sb.append("-------------------------------------------------------"); +// log.debug(sb.toString()); +// } + return Mono.just(cr); + }); + } + + private static String logStatus(ClientResponse response) { + HttpStatus status = response.statusCode(); + return String.format("Returned staus code %s (%s)", status.value(), status.getReasonPhrase()); + } + + private static String logHeaders(ClientRequest request) { + StringBuilder sb = new StringBuilder(); + + request.headers() + .forEach((name, values) -> + values.forEach(value -> sb.append(name).append(": ").append(value).append("\n")) + ); + return sb.toString(); + } + + private static String logHeaders(ClientResponse response) { + StringBuilder sb = new StringBuilder(); + response.headers() + .asHttpHeaders() + .forEach((name, values) -> + values.forEach(value -> sb.append(name).append(": ").append(value).append("\n")) + ); + return sb.toString(); + } + + private static String logMethodAndUrl(ClientRequest request) { + StringBuilder sb = new StringBuilder(); + sb.append(request.method().name()); + sb.append(" to "); + sb.append(request.url()); + + return sb.append("\n").toString(); + } +} diff --git a/src/main/java/cokr/xit/ens/core/exception/BizRuntimeException.java b/src/main/java/cokr/xit/ens/core/exception/BizRuntimeException.java new file mode 100644 index 0000000..285fd71 --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/exception/BizRuntimeException.java @@ -0,0 +1,224 @@ +package cokr.xit.ens.core.exception; + +import java.text.MessageFormat; +import java.util.Locale; + +import org.springframework.context.MessageSource; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +import cokr.xit.ens.core.exception.code.ErrorCode; +import cokr.xit.ens.core.utils.CoreSpringUtils; +import cokr.xit.ens.core.utils.MessageUtil; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +/** + *
+ * description : egov BaseRuntimeException 상속 + * packageName : kr.xit.core.exception + * fileName : BizRuntimeException + * author : julim + * date : 2023-04-28 + * ====================================================================== + * 변경일 변경자 변경 내용 + * ---------------------------------------------------------------------- + * 2023-04-28 julim 최초 생성 + * + *+ */ +@Getter +@Setter +@Slf4j +@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "Invalid parameter") +public class BizRuntimeException extends RuntimeException{ + private String code; + private String message; + private ErrorCode errorCode; + + private static final MessageUtil messageUtil = CoreSpringUtils.getMessageUtil(); + + /** + * BizRuntimeException 생성자. + * @param errorCode ErrorCode 지정 + */ + public static BizRuntimeException create(ErrorCode errorCode) { + return new BizRuntimeException(errorCode); + } + + /** + * BizRuntimeException 생성자. + * @param errorCode ErrorCode 지정 + */ + private BizRuntimeException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.code = String.valueOf(errorCode.getHttpStatus().value()); + this.errorCode = errorCode; + } + + /** + * BizRuntimeException 생성자. + * @param errorCode 에러코드 + */ + public static BizRuntimeException of(String errorCode) { + BizRuntimeException e = new BizRuntimeException(); + e.setCode(errorCode); + e.setMessage(messageUtil.getMessage(errorCode)); + return e; + } + + /** + * BizRuntimeException 생성자. + * @param defaultMessage 메세지 지정 + */ + public static BizRuntimeException create(String defaultMessage) { + return new BizRuntimeException(defaultMessage, null, null); + } + + /** + * BizRuntimeException 생성자. + * @param code + * @param defaultMessage 메세지 지정 + */ + public static BizRuntimeException create(String code, String defaultMessage) { + BizRuntimeException e = new BizRuntimeException(); + e.setCode(code); + e.setMessage(defaultMessage); + return e; + } + + public static BizRuntimeException create(String code, Object[] messageParameters) { + BizRuntimeException e = new BizRuntimeException(); + e.setCode(code); + e.setMessage(messageUtil.getMessage(code, messageParameters)); + return e; + } + + /** + * BizRuntimeException 생성자. + * @param wrappedException 원인 Exception + */ + public static BizRuntimeException create(Throwable wrappedException) { + return new BizRuntimeException("BizRuntimeException without message", null, wrappedException); + } + + /** + * BizRuntimeException 생성자. + * @param defaultMessage 메세지 지정 + * @param wrappedException 원인 Exception + */ + public static BizRuntimeException create(String defaultMessage, Throwable wrappedException) { + return new BizRuntimeException(defaultMessage, null, wrappedException); + } + + /** + * BizRuntimeException 생성자. + * @param defaultMessage 메세지 지정(변수지정) + * @param messageParameters 치환될 메세지 리스트 + * @param wrappedException 원인 Exception + */ + private static BizRuntimeException create(String defaultMessage, Object[] messageParameters, Throwable wrappedException) { + return new BizRuntimeException(defaultMessage, messageParameters, wrappedException); + } + + /** + * BizRuntimeException 기본 생성자. + */ + private BizRuntimeException() { + this("BizRuntimeException without message", null, null); + } + + /** + * BizRuntimeException 생성자. + * @param defaultMessage 메세지 지정(변수지정) + * @param messageParameters 치환될 메세지 리스트 + * @param wrappedException 원인 Exception + */ + private BizRuntimeException(String defaultMessage, Object[] messageParameters, Throwable wrappedException) { + super(wrappedException); + if(messageParameters != null) { + message = MessageFormat.format(defaultMessage, messageParameters); + }else{ + message = defaultMessage; + } + } + + /** + * BizRuntimeException 생성자. + * @param messageSource 메세지 리소스 + * @param messageKey 메세지키값 + */ + public static BizRuntimeException create(MessageSource messageSource, String messageKey) { + return new BizRuntimeException(messageSource, messageKey, null, null, Locale.getDefault(), null); + } + + /** + * BizRuntimeException 생성자. + * @param messageSource 메세지 리소스 + * @param messageKey 메세지키값 + */ + public static BizRuntimeException create(MessageSource messageSource, String messageKey, Throwable wrappedException) { + return new BizRuntimeException(messageSource, messageKey, null, null, Locale.getDefault(), wrappedException); + } + + /** + * BizRuntimeException 생성자. + * @param messageSource 메세지 리소스 + * @param messageKey 메세지키값 + * @param locale 국가/언어지정 + * @param wrappedException 원인 Exception + */ + public static BizRuntimeException create(MessageSource messageSource, String messageKey, Locale locale, Throwable wrappedException) { + return new BizRuntimeException(messageSource, messageKey, null, null, locale, wrappedException); + } + + /** + * BizRuntimeException 생성자. + * @param messageSource 메세지 리소스 + * @param messageKey 메세지키값 + * @param messageParameters 치환될 메세지 리스트 + * @param locale 국가/언어지정 + * @param wrappedException 원인 Exception + */ + public static BizRuntimeException create(MessageSource messageSource, String messageKey, Object[] messageParameters, Locale locale, Throwable wrappedException) { + return new BizRuntimeException(messageSource, messageKey, messageParameters, null, locale, wrappedException); + } + + /** + * BizRuntimeException 생성자. + * @param messageSource 메세지 리소스 + * @param messageKey 메세지키값 + * @param messageParameters 치환될 메세지 리스트 + * @param wrappedException 원인 Exception + */ + public static BizRuntimeException create(MessageSource messageSource, String messageKey, Object[] messageParameters, Throwable wrappedException) { + return new BizRuntimeException(messageSource, messageKey, messageParameters, null, Locale.getDefault(), wrappedException); + } + + /** + * BizRuntimeException 생성자. + * @param messageSource 메세지 리소스 + * @param messageKey 메세지키값 + * @param messageParameters 치환될 메세지 리스트 + * @param defaultMessage 메세지 지정(변수지정) + * @param wrappedException 원인 Exception + */ + public static BizRuntimeException create(MessageSource messageSource, String messageKey, Object[] messageParameters, String defaultMessage, Throwable wrappedException) { + return new BizRuntimeException(messageSource, messageKey, messageParameters, defaultMessage, Locale.getDefault(), wrappedException); + } + + public BizRuntimeException(MessageSource messageSource, String messageKey, Object[] messageParameters, String defaultMessage, Locale locale, Throwable wrappedException) { + super(wrappedException); + code = messageKey; + message = messageSource.getMessage(messageKey, messageParameters, defaultMessage, locale); + } + + public HttpStatus getHttpStatus(){ + return HttpStatus.BAD_REQUEST; + } + + public static void main(String[] args) { + + } +} diff --git a/src/main/java/cokr/xit/ens/core/exception/ClientErrorException.java b/src/main/java/cokr/xit/ens/core/exception/ClientErrorException.java new file mode 100644 index 0000000..8f1a5dc --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/exception/ClientErrorException.java @@ -0,0 +1,33 @@ +package cokr.xit.ens.core.exception; + +import org.springframework.http.HttpStatus; + +import cokr.xit.ens.core.utils.ApiWebClientUtil; +import lombok.Getter; + +/** + *
+ * description : WebClient 4xx 에러 : 에러 핸들러에서 사용 + * + * packageName : kr.xit.core.exception + * fileName : ClientErrorException + * author : limju + * date : 2023-05-25 + * ====================================================================== + * 변경일 변경자 변경 내용 + * ---------------------------------------------------------------------- + * 2023-05-25 limju 최초 생성 + * + *+ * @see ApiWebClientUtil + */ +@Getter +public class ClientErrorException extends RuntimeException { + private final HttpStatus status; + private final String body; + + public ClientErrorException(HttpStatus status, String body) { + this.status = status; + this.body = body; + } +} diff --git a/src/main/java/cokr/xit/ens/core/exception/ErrorParse.java b/src/main/java/cokr/xit/ens/core/exception/ErrorParse.java new file mode 100644 index 0000000..6e0d69e --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/exception/ErrorParse.java @@ -0,0 +1,99 @@ +package cokr.xit.ens.core.exception; + +import java.util.concurrent.ExecutionException; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpStatus; +import org.springframework.web.reactive.function.client.WebClientRequestException; + +import cokr.xit.ens.core.aop.ApiResponseDTO; +import cokr.xit.ens.core.utils.Checks; +import io.netty.channel.ConnectTimeoutException; +import io.netty.handler.timeout.ReadTimeoutException; + + +/** + *
+ * description : + * + * packageName : kr.xit.core.exception + * fileName : ErrorParse + * author : limju + * date : 2023-06-01 + * ====================================================================== + * 변경일 변경자 변경 내용 + * ---------------------------------------------------------------------- + * 2023-06-01 limju 최초 생성 + * + *+ */ +public class ErrorParse { + + @SuppressWarnings("rawtypes") + public static ApiResponseDTO extractError(final Throwable e){ + String errCode = String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()); + String message = Checks.isNotNull(e) ? e.getLocalizedMessage() : StringUtils.EMPTY; + HttpStatus httpStatus = null; + + if(e instanceof BizRuntimeException) { + BizRuntimeException be = (BizRuntimeException) e; + return ApiResponseDTO.error(be.getCode(), be.getMessage(), HttpStatus.BAD_REQUEST); + } + + if(e instanceof ClientErrorException) { + ClientErrorException ce = (ClientErrorException) e; + return ApiResponseDTO.error(String.valueOf(ce.getStatus()), ce.getBody(), ce.getStatus()); + } + + if(e instanceof ServerErrorException) { + ServerErrorException ce = (ServerErrorException) e; + return ApiResponseDTO.error(String.valueOf(ce.getStatus()), ce.getBody(), ce.getStatus()); + } + + // Async(React) Exception 처리 + if(e instanceof WebClientRequestException) { + if (e.getCause() instanceof ConnectTimeoutException || e.getCause() instanceof ReadTimeoutException) { + return getTimeoutException(); + } + } + + if(e instanceof ExecutionException) { + if (e.getCause() instanceof WebClientRequestException) { + message = e.getCause().getMessage(); + + // Timeout 에러 + if (e.getCause().getCause() instanceof ReadTimeoutException || e.getCause().getCause() instanceof ConnectTimeoutException) { + return getTimeoutException(); + } + } + } + + if(e instanceof ReadTimeoutException){ + return getTimeoutException(); + } + + // Async(React) Exception 처리 + if(e.getCause() instanceof WebClientRequestException){ + + // Timeout 에러 + if(e.getCause().getCause() instanceof ReadTimeoutException){ + return getTimeoutException(); + } + + } + + if(Checks.isNotEmpty(e.getCause())) { + message = e.getCause().getMessage(); + } + + return ApiResponseDTO.error(errCode, message, httpStatus); + } + + @SuppressWarnings("rawtypes") + private static ApiResponseDTO getTimeoutException(){ + return ApiResponseDTO.error( + String.valueOf(HttpStatus.REQUEST_TIMEOUT.value()), + HttpStatus.REQUEST_TIMEOUT.getReasonPhrase(), + HttpStatus.REQUEST_TIMEOUT); + } +} diff --git a/src/main/java/cokr/xit/ens/core/exception/ServerErrorException.java b/src/main/java/cokr/xit/ens/core/exception/ServerErrorException.java new file mode 100644 index 0000000..36364bf --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/exception/ServerErrorException.java @@ -0,0 +1,33 @@ +package cokr.xit.ens.core.exception; + +import org.springframework.http.HttpStatus; + +import cokr.xit.ens.core.utils.ApiWebClientUtil; +import lombok.Getter; + +/** + *
+ * description : WebClient 5xx 에러 : 에러 핸들러에서 사용 + * + * packageName : kr.xit.core.exception + * fileName : ServerErrorException + * author : limju + * date : 2023-05-25 + * ====================================================================== + * 변경일 변경자 변경 내용 + * ---------------------------------------------------------------------- + * 2023-05-25 limju 최초 생성 + * + *+ * @see ApiWebClientUtil + */ +@Getter +public class ServerErrorException extends RuntimeException { + private final HttpStatus status; + private final String body; + + public ServerErrorException(HttpStatus status, String body) { + this.status = status; + this.body = body; + } +} diff --git a/src/main/java/cokr/xit/ens/core/exception/code/ErrorCode.java b/src/main/java/cokr/xit/ens/core/exception/code/ErrorCode.java new file mode 100644 index 0000000..d96e400 --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/exception/code/ErrorCode.java @@ -0,0 +1,114 @@ +package cokr.xit.ens.core.exception.code; + +import org.springframework.http.HttpStatus; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + *
+ * description : HttpStatus 기준 에러 코드 정의 + * packageName : kr.xit.core.const + * fileName : ErrorCode + * author : julim + * date : 2023-04-28 + * ====================================================================== + * 변경일 변경자 변경 내용 + * ---------------------------------------------------------------------- + * 2023-04-28 julim 최초 생성 + * + *+ * @see HttpStatus + */ +@Getter +@RequiredArgsConstructor +@JsonFormat(shape = JsonFormat.Shape.OBJECT) +public enum +ErrorCode { + + /* +200 : OK, 요청 정상 처리 +201 : Created, 생성 요청 성공 +202 : Accepted, 비동기 요청 성공 +204 : No Content, 요청 정상 처리, 응답 데이터 없음. + +실패 +400 : Bad Request, 요청이 부적절 할 때, 유효성 검증 실패, 필수 값 누락 등. +401 : Unauthorized, 인증 실패, 로그인하지 않은 사용자 또는 권한 없는 사용자 처리 +402 : Payment Required +403 : Forbidden, 인증 성공 그러나 자원에 대한 권한 없음. 삭제, 수정시 권한 없음. +404 : Not Found, 요청한 URI에 대한 리소스 없을 때 사용. +405 : Method Not Allowed, 사용 불가능한 Method를 이용한 경우. +406 : Not Acceptable, 요청된 리소스의 미디어 타입을 제공하지 못할 때 사용. +408 : Request Timeout +409 : Conflict, 리소스 상태에 위반되는 행위 시 사용. +413 : Payload Too Large +423 : Locked +428 : Precondition Required +429 : Too Many Requests + +500 : 서버 에러 + + */ + + + BAD_REQUEST(HttpStatus.BAD_REQUEST, "요청 매개변수 오류 입니다."), + NOT_FOUND(HttpStatus.NOT_FOUND, "잘못된 요청 입니다"), + FILE_NOT_FOUND(HttpStatus.NOT_FOUND, "파일이 존재하지 않습니다"), + DATA_NOT_FOUND(HttpStatus.NOT_FOUND, "처리 데이타 오류(처리 요청 데이타 미존재)"), + + /* 400 BAD_REQUEST : 잘못된 요청 */ + CANNOT_FOLLOW_MYSELF(HttpStatus.BAD_REQUEST, "자기 자신은 팔로우 할 수 없습니다"), + + /* 401 UNAUTHORIZED : 인증되지 않은 사용자 */ + INVALID_AUTH_TOKEN(HttpStatus.FORBIDDEN, "인가된 사용자가 아닙니다"), + UN_AUTHORIZED_USER(HttpStatus.UNAUTHORIZED, "계정 정보가 존재하지 않습니다"), + AUTH_HEADER_NOT_EXISTS(HttpStatus.UNAUTHORIZED, "헤더에 인증 정보를 찾을 수 없습니다"), + LOGOUT_USER(HttpStatus.UNAUTHORIZED, "로그아웃된 사용자 입니다"), + NOT_EXISTS_SECURITY_AUTH(HttpStatus.UNAUTHORIZED, "Security Context 에 인증 정보가 없습니다"), + + NOT_EXISTS_TOKEN(HttpStatus.UNAUTHORIZED, "인증된 토큰이 없습니다"), + NOT_EXISTS_SAVED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "저장된 인증 토큰이 없습니다"), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "유효한 토큰이 아닙니다"), + INVALID_ROLE_TOKEN(HttpStatus.UNAUTHORIZED, "사용 권한이 없는 토큰 입니다"), + EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "유효기간이 경과된 토큰 입니다"), + INVALID_SIGN_TOKEN(HttpStatus.UNAUTHORIZED, "잘못된 서명의 토큰 입니다"), + INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "리프레시 토큰이 유효하지 않습니다"), + MISMATCH_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "리프레시 토큰의 유저 정보가 일치하지 않습니다"), + MISMATCH_REFRESH_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "발급된 refresh token의 정보가 일치하지 않습니다"), + NOT_EXPIRED_TOKEN_YET(HttpStatus.UNAUTHORIZED, "토큰 유효기간이 경과되지 않았습니다"), + + /* 404 NOT_FOUND : Resource 를 찾을 수 없음 */ + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자 정보를 찾을 수 없습니다"), + REFRESH_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "로그아웃 된 사용자입니다"), + + /* 409 CONFLICT : Resource 의 현재 상태와 충돌. 보통 중복된 데이터 존재 */ + DUPLICATE_RESOURCE(HttpStatus.CONFLICT, "데이터가 이미 존재합니다"), + MEMBER_EXISTS(HttpStatus.CONFLICT, "가입되어 있는 회원 입니다"), + + // JPA query error + SQL_DATA_RESOURCE_INVALID(HttpStatus.CONFLICT, "SQL 오류(데이터가 이미 존재합니다)"), + + MPOWER_CONNECT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "MPower DB 접속 에러 입니다"), + MPOWER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "MPower DB 에러 입니다"), + + FORBIDDEN(HttpStatus.FORBIDDEN, "FORBIDDEN"), + INVALID_CODE(HttpStatus.BAD_REQUEST, "유효하지 않은 HttpStatus 상태 코드 입니다"), + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_SERVER_ERROR"), + + MISMATCH_PASSWORD(HttpStatus.BAD_REQUEST, "비밀 번호가 일치하지 않습니다."), + + CONNECT_TIMEOUT(HttpStatus.REQUEST_TIMEOUT, "서버 접속 에러입니다(Connect timeout)") + ; + + + private HttpStatus httpStatus; + private String message; + + ErrorCode(HttpStatus httpStatus, String message){ + this.httpStatus = httpStatus; + this.message = message; + } +} diff --git a/src/main/java/cokr/xit/ens/core/utils/ApiWebClientUtil.java b/src/main/java/cokr/xit/ens/core/utils/ApiWebClientUtil.java new file mode 100644 index 0000000..0b9cbe9 --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/utils/ApiWebClientUtil.java @@ -0,0 +1,253 @@ +package cokr.xit.ens.core.utils; + +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.client.MultipartBodyBuilder; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.util.UriComponentsBuilder; + +import cokr.xit.ens.core.aop.ApiResponseDTO; +import cokr.xit.ens.core.config.support.WebClientConfig; +import cokr.xit.ens.core.exception.ClientErrorException; +import cokr.xit.ens.core.exception.ErrorParse; +import cokr.xit.ens.modules.kkotalk.model.CmmEnsRlaybsnmDTO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + *
+ * description : react Restfull Util + * error(.onStatus)는 ExchangeFilterFunction {@link WebClientConfig responseFilter} 에서 처리 + * packageName : kr.xit.core.spring.util + * fileName : ApiWebClientUtil + * author : julim + * date : 2023-09-06 + * ====================================================================== + * 변경일 변경자 변경 내용 + * ---------------------------------------------------------------------- + * 2023-09-06 julim 최초 생성 + * + *+ * @see WebClientConfig + * @see kr.xit.core.spring.config.AsyncExecutorConfig + * @see ClientErrorException + * @see ServerErrorException + * @see ErrorParse + * @see kr.xit.core.spring.config.support.CustomJacksonConfig + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ApiWebClientUtil { + private static final String AUTH_TYPE_BEARER = "bearer"; + private final WebClientConfig webClientConfig; + + /** + * WebClient GET 호출 처리 + * 에러(.onStatus status.is4xxClientError() || status.is5xxServerError()) + * -> {@link WebClientConfig responseFilter} 에서 처리 + * @param url String + * @param responseDtoClass Class
+ * WebClient 호출 처리 + * GET 요청시 url에 파라메터를 포함해야 함(?key=value&key2=value2) + * 에러(.onStatus status.is4xxClientError() || status.is5xxServerError()) + * -> {@link WebClientConfig responseFilter} 에서 처리 + * @param url call url + * @param method POST|GET + * @param body JSON String type + * @param rtnClzz rtnClzz return type class + * (ex: new KkopayDocDTO.DocStatusResponse().getClass()) + * @return T rtnClzz return DTO + *+ */ + public
+ * WebClient form data 호출 처리 + * -> application/x-www-form-urlencoded 전송시 + * GET 요청시 url에 파라메터를 포함해야 함(?key=value&key2=value2) + * 에러(.onStatus status.is4xxClientError() || status.is5xxServerError()) + * -> {@link WebClientConfig responseFilter} 에서 처리 + * @param url call url + * @param method POST|GET + * @param body JSON String type + * @param rtnClzz rtnClzz return type class + * (ex: new KkopayDocDTO.DocStatusResponse().getClass()) + * @return T rtnClzz return DTO + *+ */ + public
+ * description : Get Bean Object + * Filter / Interceptor 등에서 Bean 사용시 필요 + * (Bean으로 등록되는 클래스 내에서만 @Autowired / @Resource 등이 동작) + * packageName : kr.xit.core.spring.util + * fileName : CoreSpringUtils + * author : julim + * date : 2023-04-28 + * ====================================================================== + * 변경일 변경자 변경 내용 + * ---------------------------------------------------------------------- + * 2023-04-28 julim 최초 생성 + * + *+ * @see ApplicationContextProvider + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class CoreSpringUtils { + public static ApplicationContext getApplicationContext() { + return ApplicationContextProvider.getApplicationContext(); + } + + public static boolean containsBean(String beanName) { + return getApplicationContext().containsBean(beanName); + } + + public static Object getBean(String beanName) { + return getApplicationContext().getBean(beanName); + } + + public static Object getBean(Class> clazz) { + return getApplicationContext().getBean(clazz); + } + + /** + * + * @return MessageSourceAccessor + */ + public static MessageUtil getMessageUtil(){ + return (MessageUtil)getBean(MessageUtil.class); + } + + /** + * + * @return MessageSourceAccessor + */ + public static MessageSource getMessageSource(){ + return (MessageSource)getBean(MessageSource.class); + } + + public static Environment getEnvironment(){ + return (Environment)getBean(Environment.class); + } + + public static ObjectMapper getObjectMapper(){ + return (ObjectMapper)getBean(ObjectMapper.class); + } +} diff --git a/src/main/java/cokr/xit/ens/core/utils/JsonUtils.java b/src/main/java/cokr/xit/ens/core/utils/JsonUtils.java new file mode 100644 index 0000000..c7386aa --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/utils/JsonUtils.java @@ -0,0 +1,339 @@ +package cokr.xit.ens.core.utils; + +import java.io.IOException; +import java.io.StringReader; +import java.util.List; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import cokr.xit.ens.core.config.support.CustomJacksonConfig; +import cokr.xit.ens.core.exception.BizRuntimeException; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + + +/** + *
+ * description : JSON Utility + * + * packageName : kr.xit.core.support.utils + * fileName : JsonUtils + * author : limju + * date : 2023-09-04 + * ====================================================================== + * 변경일 변경자 변경 내용 + * ---------------------------------------------------------------------- + * 2023-09-04 limju 최초 생성 + * + *+ * @see CustomJacksonConfig + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class JsonUtils { + + private static final ObjectMapper OM = CoreSpringUtils.getObjectMapper(); + + /** + * json string 인지 판별 + * @param str String + * @return boolean + */ + public static boolean isJson(final String str) { + try { + OM.readTree(str); + return true; + } catch (JsonProcessingException e) { + return false; + } + } + + /** + * Object -> json string + * @return String + * @param obj Object + */ + public static String toJson(final Object obj) { + try { + return ObjectUtils.isNotEmpty(obj)? OM.writeValueAsString(obj) : null; + } catch (JsonProcessingException e) { + throw BizRuntimeException.create(e.getLocalizedMessage()); + } + } + + /** + * Json string -> class로 변환 + * @return T + * @param str String + * @param cls Class + */ + public static