Spring Boot version 2.1.x에서 2.2.x (spring frame 5.2 이상)으로 버전업 진행 시 Spring Cloud AWS SnsAutoConfiguration 에서 TypeNotPresentExceptionProxy 가 발생하며 실행안되는 문제

web/Spring|2020. 8. 21. 15:10

문제 발생


평소 문제가 많았던 webflux 부분 수정을 위해 spring boot 2.1.3에서 2.2.7버전으로 업그레이드를 진행하기 위해 gradle에서 spring boot version을 2.2.7로 변경하고 애플리케이션을 실행 시켰다. 

 

그런데 평소에는 자주본적이 없던 TypeNotPresentExceptionProxy 에러가 발생했다. 

org.springframework.beans.factory.BeanDefinitionStoreException: Failed to process import candidates for configuration class [com.baemin.bmart.search.BmartSearchAdminApplication]; nested exception is java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy
	at org.springframework.context.annotation.ConfigurationClassParser.processImports(ConfigurationClassParser.java:609)
	at org.springframework.context.annotation.ConfigurationClassParser.access$800(ConfigurationClassParser.java:110)
	at org.springframework.context.annotation.ConfigurationClassParser$DeferredImportSelectorGroupingHandler.lambda$processGroupImports$1(ConfigurationClassParser.java:811)
	at java.util.ArrayList.forEach(ArrayList.java:1257)

에러가 발생한 호출 스택 정보를 보니 ConfigurationClassParser에 processImports에서 발생한걸로 보아 @Configuration 어노테이션이 적용된 클래스에서 빈정보를 import하다가 에러가 발생한 것 같았다.

 

그래서 어떤 에러로 인해 발생한 것인지 확인해보기 위해서 TypeNotPresentExceptionProxy 클래스에 디버깅을 찍고 문제가 발생한 로그를 확인해봤다.

 

java.lang.NoClassDefFoundError org/springframework/web/servlet/config/annotation/WebMvcConfigurer 가 발행했다.

 

WebMvcConfigurer NoClassDefFoundError가 발생할 이유가 없는데 발생하여 난감해하고 있었는데 확인해보니 내부적으로 사용중이던 compile 'org.springframework.cloud:spring-cloud-aws-messaging' 라이브러리에서 SnsWebConfiguration이 있는데 이게 2.1.0버전에는 아래와 같이 구현되어 있다.

@Configuration
@ConditionalOnClass("org.springframework.web.servlet.config.annotation.WebMvcConfigurer")
public class SnsWebConfiguration implements WebMvcConfigurer {

    @Autowired
    private AmazonSNS amazonSns;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(getNotificationHandlerMethodArgumentResolver(this.amazonSns));
    }
}

여기서 Class가 class path에 잘 있는지 확인하는 @ConditionalOnClass 어노테이션의 value에 WebMvcConfigurer 클래스의 존재를 체크하도록 설정되어 있다.

 

@ConditionalOnClass의 경우에는 해당 클래스가 잘 있으면 Bean으로 등록하라는 하나의 조건인데 만약 WebMvcConfigurer이 없으면 해당 Configuraion을 안쓰면 되는데 왜 죽은거서 인가 하고 의아했다.

 

여기서 같은 실에 개발자인 용근님이 올리신 issue를 보고 알게 되었다. 

https://github.com/spring-cloud/spring-cloud-aws/issues/549

https://github.com/spring-cloud/spring-cloud-aws/issues/503

 

ArrayStoreException in SnsWebConfiguration. · Issue #549 · spring-cloud/spring-cloud-aws

Hello, Because implement and ConditionalOnClass are specified in SnsWebConfiguration at the same time, an exception occurs during the class load. When WebMvcConfigurer does not exist, Caused by: ja...

github.com

SnsWebConfiguration에서 WebMvcConfigurer를 implements하고 있는데 해당 classpath를 찾을 수 없어 발생한 오류입니다.

 

그런데 왜 단순히 spring boot version만 올렸고 spring cloud aws 버전은 2.1.0버전 그대로 사용하고 있는데, 이게 2.1.3버전에서는 발생하지 않고 2.2.7버전에서만 발생한건지 궁금해서 원인을 찾아보았다.

 

 

 

원인분석


우선 두 개의 버전에서 어떤 차이가 있는지 정확하게 알지 못하기에 spring boot가 뜨기 위해서 호출하는 ConfigurationConfigurationClassParser 부분을 집중적으로 확인해봤다.

 

두 개의 버전을 사용하는 모듈을 동시에 실행시켜 놓고 ConfigurationClassParser클래스에 processImports부분을 살펴봤다. 디버깅 코드를 찍고 오류가 시작되는 부분을 찾고 호출스택을 하나씩 역추적해봤다. 

 

의미있는 부분부터 정리하면 processImports 메소드가 호출되고 내부 코드에서 아래 processConfigurationClass를 호출하고 그 내부에서 doProcessConfigurationClass메소드를 다시 호출한다.

 

이 과정을 통해 configuration class들에 대한 메타데이터 정보를 수집하게 되는데 이때 메타데이터를 수집하기 위해서 사용되는 클래스가 SimpleMetadataReader 이다.

 

아래 두 개의 코드를 보면 차례로 2.1.3 버전의 코드와 2.2.7버전의 SimpleMetadataReader를 확인할 수 있다.

SimpleMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException {
   InputStream is = new BufferedInputStream(resource.getInputStream());
   ClassReader classReader;
   try {
      classReader = new ClassReader(is);
   }
   catch (IllegalArgumentException ex) {
      throw new NestedIOException("ASM ClassReader failed to parse class file - " +
            "probably due to a new Java class file version that isn't supported yet: " + resource, ex);
   }
   finally {
      is.close();
   }

   AnnotationMetadataReadingVisitor visitor = new AnnotationMetadataReadingVisitor(classLoader);
   classReader.accept(visitor, ClassReader.SKIP_DEBUG);

   this.annotationMetadata = visitor;
   // (since AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor)
   this.classMetadata = visitor;
   this.resource = resource;
}
SimpleMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException {
   SimpleAnnotationMetadataReadingVisitor visitor = new SimpleAnnotationMetadataReadingVisitor(classLoader);
   getClassReader(resource).accept(visitor, PARSING_OPTIONS);
   this.resource = resource;
   this.annotationMetadata = visitor.getMetadata();
}

private static ClassReader getClassReader(Resource resource) throws IOException {
	try (InputStream is = resource.getInputStream()) {
		try {
			return new ClassReader(is);
		}
		catch (IllegalArgumentException ex) {
			throw new NestedIOException("ASM ClassReader failed to parse class file - " +
				"probably due to a new Java class file version that isn't supported yet: " + resource, ex);
		}
	}
}

두 개 생성자를 보면 ClassReader를 가지고 오는 부분이 메서드로 2.2.7버전에서 빠진것을 제외해보면 다른 부분이 MetadataReadingVisitor 클래스가 다르다는 것이다.

 

2.1.3버전에서는 AnnotationMetadataReadingVisitor를 사용하고 2.2.7버전에서는 SimpleAnnotationMetadataReadingVisitor를 사용하는데 AnnotationMetadataReadingVisitor은 spring 5.2버전 부터 deprecated되었다.

 * @deprecated As of Spring Framework 5.2, this class has been replaced by
 * {@link SimpleAnnotationMetadataReadingVisitor} for internal use within the
 * framework, but there is no public replacement for
 * {@code AnnotationMetadataReadingVisitor}.
 */

 

그럼 이 MetadataReadingVisitor클래스가 무엇인지 확인해보자. 이 클래스들에 대한 설명은 아래와 같은데 정리해보면 AnnotaionMetadata 인터페이스를 통해 공개되어 있는 클래스 이름과 구현된 클래스에 정의되어 있는 어노테이션 뿐만 아니라 구현된 타입을 찾기 위한 ASM Class visitor 라고 설명이 되어있다.

* ASM class visitor which looks for the class name and implemented types as
* well as for the annotations defined on the class, exposing them through
* the {@link org.springframework.core.type.AnnotationMetadata} interface.

ASM은 자바 바이트코드를 조작하고 분석하는 프레임워크인데 위에 MetadataReadingVisitor들은 결국 Configuration class import parse를 하는 작업에서 메타 데이터를 가지고 오는 SimpleMetadataReader에서 class들에 대한 정보를 가지고 오기 위한 역할을 하는 클래스들이다.

 

상위 추상클래스 ClassVisitor의 메소드를 살펴보면 visit, visitSource, visitModule, visitOuterClass, visitAnnotation, visitTypeAnnotation, visitAttribute, visitInnerClass, visitEnd등등 클래스에 대한 정보를 가져올 때 사용되는 것이라는걸 확인할 수 있다.

 

이 때 에러가 발생되면서 애플리케이션이 죽냐 안죽냐가 결정이 되는걸로 봐서 이쪽에 처리가 서로 다르게 되어있을 거라고 짐작하고 더 확인해봤다.

 

우선 2.1.3 버전에 경우에는 visitEnd() 메소드를 호출할 때 에러가 발생하게 되는데 AnnotationMetadataReadingVisitor의 visitEnd 메소드는 우선 아래와 같이 되어있다.

@Override
public void visitEnd() {
   super.visitEnd();

   Class<? extends Annotation> annotationClass = this.attributes.annotationType();
   if (annotationClass != null) {
      List<AnnotationAttributes> attributeList = this.attributesMap.get(this.annotationType);
      if (attributeList == null) {
         this.attributesMap.add(this.annotationType, this.attributes);
      }
      else {
         attributeList.add(0, this.attributes);
      }
      if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotationClass.getName())) {
         try {
            Annotation[] metaAnnotations = annotationClass.getAnnotations();
            if (!ObjectUtils.isEmpty(metaAnnotations)) {
               Set<Annotation> visited = new LinkedHashSet<>();
               for (Annotation metaAnnotation : metaAnnotations) {
                  recursivelyCollectMetaAnnotations(visited, metaAnnotation);
               }
               if (!visited.isEmpty()) {
                  Set<String> metaAnnotationTypeNames = new LinkedHashSet<>(visited.size());
                  for (Annotation ann : visited) {
                     metaAnnotationTypeNames.add(ann.annotationType().getName());
                  }
                  this.metaAnnotationMap.put(annotationClass.getName(), metaAnnotationTypeNames);
               }
            }
         }
         catch (Throwable ex) {
            if (logger.isDebugEnabled()) {
               logger.debug("Failed to introspect meta-annotations on " + annotationClass + ": " + ex);
            }
         }
      }
   }
}

 

여기서 annotaion 정보를 가지고 올 때 TypeNotPresentExceptionProxy에러가 동일하게 발생한다.

 

하지만 여기서 2.2.7버전과의 차이점은 이 AnnotationMetadataReadingVisitor 경우에는 에러가 발생했을 때 내부적으로 로깅만 하고 상위로 에러를 전파하지 않는다는 점이다. 그래서 결국 에러가 발생하지 않고 로깅만 되고 애플리케이션이 실행되는데 까지는 문제가 없었고 내부적으로 Sns 기능을 쓰지 않기에 문제가 없었던 것이었다.

 

 

 

그럼 이제 2.2.7버전에서는 어떤지 확인해보자. 

2.2.7에서 사용하는 SimpleAnnotationMetadataReadingVisitor의 경우에는 클래스에 정보를 얻기위해 작업하는 visit와 같은 동작들에 대해서 별도의 try catch 작업이 되어 있지 않다.

 

실제로 에러가 발생되는 visitAnnotation메소드를 보면 이 곳에서는 visitAnnotaion을 통해 정보를 가져올 때 내부적으로 MergedAnnotaionReadingVisitor visitor를 사용하지만 별도에 에러에 대한 처리가 되어 있지 않는걸 볼 수 있다. 

@Override
@Nullable
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
	return MergedAnnotationReadingVisitor.get(this.classLoader, this::getSource,
			descriptor, visible, this.annotations::add);
}

 

그로 인해 visitor를 통해 configuration class에 내부정보를 가져오다가 에러가 발생하게 되면 processImport에 에러가 전파되어 BeanDefinitionStoreExeption이 발생하면서 결국 application이 실행이 되지 않는 것이다.

ConfigurationClassParser클래스 내부에 processImports 코드

 

 

 

해결방법


문제가 어찌되었든 우리는 spring boot의 버전을 올려야한다. 그래서 정식으로 수정이 완료된 2.2.3버전을 사용을 하던지 또는 임시로 webmvc 라이브러리를 추가해주는 것이다.

 

 

해결방법 1.

https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-aws-messaging

// https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-aws-messaging
compile group: 'org.springframework.cloud', name: 'spring-cloud-aws-messaging', version: '2.2.3.RELEASE'

 

해결방법 2.

compile('org.springframework:spring-webmvc')

 

 

댓글()

spring cloud resilience4j 사용시 CircuitBreakerConfiguration 에러

web/Spring|2020. 2. 23. 20:32

CircuitBreaker 테스트를 위해서 Resilience4j를 사용했다.

버전은 1.3.0을 사용하려고 했다.

//Resilience4J
compile("io.github.resilience4j:resilience4j-spring-boot2:1.3.0")
compile("io.github.resilience4j:resilience4j-reactor:1.3.0")
compile("io.github.resilience4j:resilience4j-timelimiter:1.3.0")

 

그런데 분명 1.3.0을 사용한다고 명시하였고 gradle도 clean하고 사용하는 denpendency도 확인하였는데 계속해서 다음과 같이 1.1.0 라이브러리를 사용하려고 해서 문제가 발행했다.

Cannot resolve method 'of(java.util.Map<java.lang.String,io.github.resilience4j.circuitbreaker.CircuitBreakerConfig>, io.github.resilience4j.core.registry.RegistryEventConsumer<io.github.resilience4j.circuitbreaker.CircuitBreaker>, io.vavr.collection.HashMap<K,V>)'

 

그래서 알아보던 중 반갑게도 git에 해당 내용에 대한 이슈가 있었다.

이유는 spring cloud를 dependencyManamement로 선언해서 사용해서였다. spring cloud에 경우 내부적으로 1.1.0 라이브러리를 사용하고 있기 때문에 버전 충돌이 나서 그런것이었다. ㅋㅋ

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

 

에휴 한시간을 방황한 것 같다.

https://github.com/resilience4j/resilience4j/issues/596#issuecomment-582697615

댓글()

클라우드 컴퓨팅 설명 및 서비스 종류 정리

IT 지식/cloud|2018. 7. 13. 22:15




클라우드 서비스

아마존과 MS, 구글 뿐만 아니라 국내에서는 NHN, 카페 25시등 클라우딩 컴퓨팅 사업에 뛰어들고 있다. 대체 클라우드가 왜 인기가 있고 클라우드가 무엇인지 알아보자.


클라우드란?

클라우드는 사용자가 서비스를 사용한 만큼의 비용을 지불하면서 서비스를 사용하는 것을 말하고 언제 어디서든 서비스를 접근하여 사용할 수 있는 서비스를 말한다.


인기 이유

인터넷이 보급되고 각 가정에 컴퓨터와 IT 기기들이 대중화 되면서 그 인기와 함께 기술도 같이 향상되었다. 나 또한 IT 업계에서 3년정도 일을 하고 있다.

세상이 편해지고 주머니가 가볍게 편하게 모든 서비스를 이용하고 싶어 카카오 페이, 삼성페이등도 넘처나는 시대가 되었다. 컴퓨터 역시 예전에는 사람들이 노트북을 들고다니고 별도의 소프트웨어를 컴퓨터에 설치하고 다녔으나 이제는 모두 클라우딩으로 해결할 수 있게 되었다. 바로 이또한 웹이 발전하게 되면서 가능했다고 본다.

그럼 기업들은 왜 열광하게 되었을까? 기업의 경우 서비스를 도입할 때 물리적 장비와 관리 포인트를 생각하지 않을수가 없다. 기존의 경우 IDC센터에 돈을 주면서 서버관리 및 여러 서비스를 지원받거나 직접 구축해서 운영하였다. 하지만 넥플릭스와 같은 거대기업들도 이제는 aws 클라우드에 모든 호스팅을 지원받아 운영하고 있다.

관리 포인트가 줄고 물리적 장비를 별도로 관리하지 않아도 되며, 간단하게 운영이 가능하다는 면에서 클라우드 컴퓨팅이 인기가 되고 있다.


서비스 종류

SaaS (Software as a Service)
  • 가장 일반적인 유형의 클라우드 서비스이며 서비스를 제공하는 곳에서 인프라와 소프트웨어까지 모두 제공한다. 웹 메일이나, office 365, 구글 드라이브 등이 해당된다.

IaaS (Ifnfrastructure as Service)
  • 가상 서버, 데이터 스토리지 및 호스팅 컴퓨터, 네트워크 등 IT 인프라를 지원해주는 서비스이다. 대표적으로 성공한 것이 AWS이다.

PaaS (Platform as a Service)
  • 기본적인 IaaS는 물론 개발툴과 기능 애플리케이션 배포까지 제공해준다. 즉  개발자가 애플리케이션을 개발하고 배포하는데 필요한 모든 것을 제공한다. 대표적으로 Google App Engine, Oracle Cloud Platform, Cloud Foundry등이 존재한다.
DaaS (Desktop as a Service)
클라우드 데스크탑 Toast PC처럼 데스크탑 서비스를 사용하는 것. http://m.ciobiz.co.kr/20160811120010

'IT 지식 > cloud' 카테고리의 다른 글

오토 스케일링(Auto Scaling) 소개  (0) 2018.07.14
클라우드 컴퓨팅 설명 및 서비스 종류 정리  (0) 2018.07.13

댓글()

Spring Rest Client Feign 소개

web/Spring|2018. 5. 27. 18:16


Feign는 넷플릭스에서 개발된 HTTP 클라이언트다.

Feign은 HTTP API 클라이언트를 간단하게 제공한다. Feign을 사용하기 위해서는 인터페이스를 선언하고, 어노테이션화 하면 이를 런타임에서 실제 구현을 제공된다.

Feign 라이브러리 추가
- feign-okhttp는 request를 만들기 위해서 내부적으로 Square OkHttp 클라이언트를 사용하기 위해 사용
- feign-gson은 JSON 처리기로서 Google Gson을 사용하기 위해 로드
- feign-slf4j는 request들을 로깅하기 위해서 라이브러리 사용



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
    <version>9.3.1</version>
</dependency>
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-gson</artifactId>
    <version>9.3.1</version>
</dependency>
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-slf4j</artifactId>
    <version>9.3.1</version>
</dependency>
cs





Feign 인터페이스 추가
- Feign 클라이언트는 텍스트 기반 HTPP API이다. 그렇기 때문에 파일 다운로드나 업로드를 위한 binary data는 다룰 수 없다.
- 인터페이스에 메소드에 @ReqeustLine에 요청 method와 url을 기재해 준다.
- RequestLine에 기재된 사용 방법을 보면 다음과 같다.



1
2
@RequestLine("GET /servers/{serverId}?count={count}")
void get(@Param("serverId") String serverId, @Param("count") int count);
cs



아래 Wedulpos에서 사용하는 API를 호출하는 인터페이스를 만들었다.




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.wedul.common.util.rest;
 
import feign.RequestLine;
 
/**
 * Http Request Client
 * 
 * @author wedul
 *
 */
public interface RestClientI {
 
    @RequestLine("POST /password/find")
    String findPassword();
    
}
cs





Feign Client 호출
- 인터페이스 기반의 클리이언트를 구성하기 위해서 Feign.builder()을 사용할 수 있다.
- JSON/XML 등 여러 encoder와 decoder를 사용 할 수 있다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.wedul.wedulpos.user.test;
 
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import com.wedul.common.util.rest.RestClientI;
import com.wedul.wedulpos.user.CommonTestConfiguration;
 
import feign.Feign;
import feign.Logger;
import feign.gson.GsonDecoder;
import feign.gson.GsonEncoder;
import feign.okhttp.OkHttpClient;
import feign.slf4j.Slf4jLogger;
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes= {CommonTestConfiguration.class})
public class FeignTest {
    
    RestClientI restClientI;
    
    @Before
    public void setUp() throws Exception {
        restClientI = Feign.builder()
                  .client(new OkHttpClient())
                  .encoder(new GsonEncoder())
                  .decoder(new GsonDecoder())
                  .logger(new Slf4jLogger(RestClientI.class))
                  .logLevel(Logger.Level.BASIC)
                  .target(RestClientI.class"http://localhost:8080/wedulpos/user");
    }
    
    @Test
    public void feignTest() {
        System.out.println("Feign test result : " + restClientI.findPassword());
    }
 
}
cs




https 도 가능하다
자세한 예제는 https://github.com/OpenFeign/feign 참조.








댓글()
  1. hojak99 2018.07.26 17:43 댓글주소  수정/삭제  댓글쓰기

    감사합니다