Spring boot와 Docker

spring boot와 Docker

일단 도커를 설치하자
설치 방법은 설치 리눅스(centos6.5) 기준이다.
일단 Spring boot 프로젝트를 만들자.
만드는법은 Spring boot 빠르게 시작해보자
혹은 Github example

@SpringBootApplication
@RestController
public class SpringBootDockerApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootDockerApplication.class, args);
    }

    @RequestMapping("/")
    public String hello(){
        return "Hello Spring boot docker!";
    }
}

메인 소스다. 간단하게 만들었다.
그리고 추가 할 부분은
src/main/dockerDockerfile 파일을 만든다.

FROM java:8
VOLUME /tmp
ADD spring-boot-docker-0.0.1-SNAPSHOT.jar app.jar
RUN bash -c 'touch /app.jar'
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

추가하자.
다음은 maven 플러그인을 설정하자

<properties>
    <docker.image.prefix>wonwoo</docker.image.prefix>
</properties>

...


<plugin>
    <groupId>com.spotify</groupId>
    <artifactId>docker-maven-plugin</artifactId>
    <version>0.2.3</version>
    <configuration>
        <imageName>${docker.image.prefix}/${project.artifactId}</imageName>
        <dockerDirectory>src/main/docker</dockerDirectory>
        <resources>
            <resource>
                <targetPath>/</targetPath>
                <directory>${project.build.directory}</directory>
                <include>${project.build.finalName}.jar</include>
            </resource>
        </resources>
    </configuration>
</plugin>

그런다음에 빌드를하자
메이븐 기준이라…

mvn package && java -jar target/spring-boot-docker-0.0.1-SNAPSHOT.jar
#필자는 클라우드서버라 포트를 변경해야되서..
#mvn package && java -Dserver.port=9000 -jar target/spring-boot-docker-0.0.1-SNAPSHOT.jar 

웹브라우저를 열고 확인하자!
Hello Spring boot docker!
정상적으로 되는걸 확인했으니 이제 도커를 빌드하자.

mvn package docker:build

그럼 아래와 같은 화면이 나올 것이다.
이미지를 생성하는듯하다. 자바도 pull하는듯 하다. 흠

....

[INFO] Building image wonwoo/spring-boot-docker
Step 0 : FROM java:8
Pulling from java
d8bd0657b25f: Extracting [======================>                            ] 23.07 MB/51.37 MB
a582cd499e0f: Download complete 
3c3e582d88fa: Download complete 
5901462573ab: Download complete 
87d3bfd91a40: Download complete 
337c6b2193cb: Download complete 
c9f473494918: Download complete 
6d2585cde477: Download complete 
c49cfc438d8b: Download complete 
1d7d8f54c2b3: Download complete 
5f59c75f3075: Download complete 
7de249ebc2b5: Download complete 
7e810ba21977: Downloading [===================>                               ] 51.89 MB/129.9 MB
31e7de89e3f8: Download complete 

Status: Downloaded newer image for java:8
---> 31e7de89e3f8
Step 1 : VOLUME /tmp
---> Running in 22eb30aaef5d
---> 92198c129da9
Removing intermediate container 22eb30aaef5d
Step 2 : ADD spring-boot-docker-0.0.1-SNAPSHOT.jar app.jar
---> addd5f953ba5
Removing intermediate container a4fea7ea6ed4
Step 3 : RUN bash -c 'touch /app.jar'
---> Running in f21125980342
---> 390d62487fa3
Removing intermediate container f21125980342
Step 4 : ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -jar /app.jar
---> Running in dc04d29c1616
---> 40ac0290f2de
Removing intermediate container dc04d29c1616
Successfully built 40ac0290f2de

이미지도 생성 되었으니 실행해보자.
형식은 이렇다.
docker run -p $HOSTPORT:$CONTAINERPORT -t $IMAGE

docker run -p 8080:8080 -t wonwoo/spring-boot-docker
#필자는 외부가 9000이라..
#docker run -p 9000:8080 -t wonwoo/spring-boot-docker

브라우저를 띄우면 성공적으로 됐다.
컨테이너를 한개 더 생성해보자.

docker run -p 8081:8080 -t wonwoo/spring-boot-docker
#docker run -p 9001:8080 -t wonwoo/spring-boot-docker

브라우저를 띄워서 다시 보자
두개 다 정상적으로 올라왔다.

컨테이너정보를 확인해보자

docker ps

CONTAINER ID        IMAGE                         COMMAND                CREATED             STATUS              PORTS                    NAMES
2296052db0f5        wonwoo/spring-boot-docker   "java -Djava.securit   6 minutes ago       Up 6 minutes        0.0.0.0:9001->8080/tcp   trusting_turing      
d927a7115967        wonwoo/spring-boot-docker   "java -Djava.securit   7 minutes ago       Up 7 minutes        0.0.0.0:9000->8080/tcp   distracted_galileo

요런식으로 두개가 올라와 있다.
한개를 정지해보자
정지하는건 docker stop $CONTAINER_ID

docker stop d927a7115967

다시 확인해보면

CONTAINER ID        IMAGE                         COMMAND                CREATED             STATUS              PORTS                    NAMES
2296052db0f5        wonwoo/spring-boot-docker   "java -Djava.securit   7 minutes ago       Up 7 minutes        0.0.0.0:9001->8080/tcp   trusting_turing

요렇게 한개만 있다.

다시 실행해보자

docker start d927a7115967

다시 실행 된걸 알 수 있다.

간단한 커멘드를 알아보자.

docker images : 이미지를 보여준다.
docker ps -a : 모든 컨테이너 정보다.
docker rm $CONTAINER_ID : 컨테이너를 삭제한다.
docker start $CONTAINER_ID : 컨테이너를 시작한다.
docker stop $CONTAINER_ID : 컨테이너를 중지한다. 
docker logs $CONTAINER_ID : 로그를 확인한다.
docker top $CONTAINER_ID : 프로세서 정보를 확인한다.
docker inspect $CONTAINER_ID : 컨테이너의 모든 정보를 보여준다.(JSON)
docker port $CONTAINER_ID : 포트가 어디로 연결 되었있는지 보여준다.

이 외에도 많은 커멘드가 있다.

docker help 

로 통해 확인하자!

참고 : 만약 docker hub 에 push를 날릴려면 wonwoo를 docker에 가입할때 본인의 name으로 바꾸셔야됩니다!!

Spring boot의 ApplicationContext

Spring boot ApplicationContext

인터넷을 보다보니까 좋은 자료가 있어 한번 보게 되었다.
Spring ApplicationContext
스터디 하는 그룹인데 아라한사님도 여기 계신다.
보다보니 Spring boot는 어떤식으로 되는지 궁금했다.
Spring boot는 기본적으로 AnnotationConfigApplicationContext을 사용하고 있다.
Web이라면 AnnotationConfigEmbeddedWebApplicationContext을 사용한다.
물론 둘다 아무 설정이 안되어 있을때 이야기다.

protected ConfigurableApplicationContext createApplicationContext() {
    Class<?> contextClass = this.applicationContextClass;
    if (contextClass == null) {
        try {
            contextClass = Class.forName(this.webEnvironment
                    ? DEFAULT_WEB_CONTEXT_CLASS : DEFAULT_CONTEXT_CLASS);
        }
        catch (ClassNotFoundException ex) {
            throw new IllegalStateException(
            "Unable create a default ApplicationContext, "
                + "please specify an ApplicationContextClass", ex);
        }
    }
    return (ConfigurableApplicationContext) BeanUtils.instantiate(contextClass);
}

이렇게 보면 알 수 있다.
그런데 인스턴스를 바로 생성하기 때문에 register(annotatedClasses) 빈의 메타정보를 담는 이 메소드를 호출 하지 않는다. 아니면 java 설정 중 register(annotatedClasses)을 명시적으로 호출해 줘야하는데…
메인이 되는 클래스를 설정해야되는데..어디서하지? 어딘가엔 있을거라고 장담하고 찾아봤다.
SpringbootApplication 클래스에 보면 createAndRefreshContext 함수가 있다. 여기가 아주 중요한 역할을 해주는 함수인듯 하다.
여기서 load라는 함수를 따라가보면 계속 load다. 계속 load 만 따라가면 된다. 찾았다 요놈

private int load(Class<?> source) {
    if (isGroovyPresent()) {
        // Any GroovyLoaders added in beans{} DSL can contribute beans here
        if (GroovyBeanDefinitionSource.class.isAssignableFrom(source)) {
            GroovyBeanDefinitionSource loader = BeanUtils.instantiateClass(source,
                    GroovyBeanDefinitionSource.class);
            load(loader);
        }
    }
    if (isComponent(source)) {
               //잡았다 요놈
        this.annotatedReader.register(source); 
        return 1;
    }
    return 0;
}

register 함수를 따라가보면 마지막 registerBean라는 메소드가 있는데
BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
요놈이 빈의 메타 정보를 저장하는놈이다. 아마 저기 링크 걸어둔 곳에 자세히 나와있다.
그리고 createAndRefreshContext 메소드에 refresh 메소드도 호출 하고 있다.
그래서 한번 따라가봤다. refresh에서 많은 일을 하고 있다.
refresh에 invokeBeanFactoryPostProcessors를 따라가 보았다. 복잡하다..ㅜㅜ

PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());

안으로 들어가서 약 100 번째줄에

invokeBeanDefinitionRegistryPostProcessors(priorityOrderedPostProcessors, registry)

이런 메소드가 존재한다. 계속따라가보면 따라가기 힘들다.

for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
    postProcessor.postProcessBeanDefinitionRegistry(registry);
}

구현체는 ConfigurationClassPostProcessor로 가면된다.
함수 맨 밑에 가면 processConfigBeanDefinitions 메소드가 있는데 여러작업을 한다. 순서를 정렬하고, @Configuration 클래스도 파싱하는 듯하다.
그중에 parser.parse(candidates) 메소드는 ComponentScan 하는곳을 찾을수 있다. 따라가보자
parse -> processConfigurationClass -> doProcessConfigurationClass 여기 설정 클래스들이 모여있다.
이쁘게 주석도 해놨다.
@Import, @Bean, @ImportResource, @PropertySource등을 찾아서 메타정보를 등록한다.
doProcessConfigurationClass 함수는 재귀호출하는듯 하다.
스캔대상의 클래스들을 하나씩 다 조사한다.
그리고 configurationClasses에 Map으로 설정클래스랑 클래스에 설정된 빈(BeanMethod), 기타 다른 설정 빈들이 담아둔다.
invokeBeanFactoryPostProcessors 뭔가 많은 일은한다. 복잡복잡
여기 안에서 BeanFactoryPostProcessorBeanDefinitionRegistryPostProcessor 의 구현체들도 호출한다.
BeanDefinitionRegistryPostProcessor 먼저 호출 되고 그다음에 BeanFactoryPostProcessor 호출 된다.
ConfigurationClassPostProcessor 도 BeanDefinitionRegistryPostProcessor의 구현체다.
순서에 따라 각각 세번정도 호출 되는거 같다.(PriorityOrdered, Ordered)

private static void invokeBeanDefinitionRegistryPostProcessors(
        Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {

    for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
        postProcessor.postProcessBeanDefinitionRegistry(registry);
    }
}

refresh의 finishBeanFactoryInitialization Instantiate all remaining (non-lazy-init) singletons. 주석으로 이렇게 되어있다.
아마 인스턴스 안된 빈들을 이때 인스턴스화 하는거 같다.(Lazy 객체가 아닌 것만)
이렇게 Spring boot는 Application의 관리 및 설정에 관하여 알아봤다.
인스턴스화 하는건 저기 링크에 가보면 자세히 나와있다! 링크참조!
필자의 개인적인 공부기에 틀릴 수도 있다.
개인적인 생각으로…. 어렵다.
머리좋은놈들이 만들어서 ㅜㅜ

spring-boot-rest를 해보자!(2)

Spring boot rest 를 이용하여 API 서버를 개발해보자! (2)

1편은 여기

검색을 할때 url에 메소드명이 마음에 들지 않는다. 또한 json 키도 마음에 들지 않는다. 그래서 바꾸고싶다.
그러기 위해선 아래와같이 추가해보자

    @RestResource(path = "nameStartsWith", rel = "name")
    Page<Account> findByNameStartsWith(@Param("name") String name, Pageable pageable);

브라우저로 열어보자
http://localhost:8080/account/search

{
  "_links": {
    "name": { //name으로 변경
      "href": "http://localhost:8080/account/search/nameStartsWith{?name,page,size,sort}", //nameStartsWith 으로 변경
      "templated": true
    },
    "findByname": {
      "href": "http://localhost:8080/account/search/findByname{?first_name,page,size,sort}",
      "templated": true
    },
    "self": {
      "href": "http://localhost:8080/account/search"
    }
  }
}

이렇게 나온걸 확인할 수 있다. 그리고 http://localhost:8080/account/search/nameStartsWith?name=wonwoo 로 들어가보면 wonwoo로 시작하는 데이터가 두개 나올 것이다.
아니 근데 나는 이메일을 안보여주고 싶은 경우도 있다. 흠!
그렇담 아래와 같이 인터페이스 추가하자

@Projection(name = "noEmail", types = { Account.class })
public interface NoEmailAccount {

    String getId();

    String getName();

    String getPassword();
}

그런다음 http://localhost:8080/account/1?projection=noEamil url로 브라우저를 열어보자
그러면 아래와 같이 email이 빠진걸 확인 할 수 있다.

{
  "name": "wonwoo",
  "id": "1",
  "password": "qwer",
  "_links": {
    "self": {
      "href": "http://localhost:8080/account/1"
    },
    "account": {
      "href": "http://localhost:8080/account/1{?projection}",
      "templated": true
    }
  }
}

다음은 Address라는 엔티티를 추가해서 Account 에 주소로 사용할 예정이다.

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Address {
    @Id
    @GeneratedValue
    @Column(name = "address_id")
    private Long id;

    private String street;

    private String state;

    private String country;
}

그리고 Account 엔티티에 다음과 같이 추가 하자

    @OneToOne
    @JoinColumn(name="address_id")
    private Address address;

그리고 초기화 데이터를 넣어주자!

insert into address(address_id, street, state, country) values(1L, '분당구', '경기도', '대한민국');
insert into address(address_id, street, state, country) values(2L, '강남구', '서울특별시', '대한민국');
insert into account(id, name, email, password, address_id) values(1,'wonwoo','wonwoo@test.com','qwer', 1L);
insert into account(id, name, email, password, address_id) values(2,'kevin','kevin@test.com','asdf', 2L);
insert into account(id, name, email, password, address_id) values(3,'wonwoo1','kevin@test.com','qwqw',1L);

그리고 나서 http://localhost:8080/account/1 확인해보자

{
  "name": "wonwoo",
  "email": "wonwoo@test.com",
  "password": "qwer",
  "address": {
    "street": "분당구",
    "state": "경기도",
    "country": "대한민국"
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/account/1"
    },
    "account": {
      "href": "http://localhost:8080/account/1{?projection}",
      "templated": true
    }
  }
}

이렇게 제대로 나올 것이다. 흠. 근데 address가 마음에 들지 않는다. 한줄로도 나왔으면 좋겠다.
그럼 아까 했던 @Projection을 응용해서 보자
아래와 같이 FullAddress라는 인터페이스 생성한다.

@Projection(name = "fullAddress", types = { Account.class })
public interface FullAddress {

    @Value("#{target.address.country} #{target.address.state} #{target.address.street}")
    String getFullAddress();

    String getName();

    String getEmail();

    String getPassword();
}

그리고 나서 http://localhost:8080/account/1?projection=fullAddress 확인해보면 아래와 같이 한줄로 나왔다.

{
  "name": "wonwoo",
  "password": "qwer",
  "email": "wonwoo@test.com",
  "fullAddress": "대한민국 경기도 분당구",
  "_links": {
    "self": {
      "href": "http://localhost:8080/account/1"
    },
    "account": {
      "href": "http://localhost:8080/account/1{?projection}",
      "templated": true
    }
  }
}

그럼 spring-data-rest-jpa 여기서 마치겠다.
소스는 https://github.com/wonwoo/spring-data-rest-jpa 다운 받을 수 있다
다음엔 몽고도 알아보겠다.