spring restdoc 을 만들어 보자

spring restdoc 을 만들어 보자

restdoc을 쓸수도 있을 거 같아서 정리 한다. spring-boot 기준으로 작성 하였다.
기본적인 spring-boot를 안다고 가정하고 작성한다.


... <dependency> <groupId>org.springframework.restdocs</groupId> <artifactId>spring-restdocs-mockmvc</artifactId> <version>1.0.1.RELEASE</version> </dependency> .... <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <includes> <include>**/*Documentation.java</include> </includes> </configuration> </plugin> <plugin> <groupId>org.asciidoctor</groupId> <artifactId>asciidoctor-maven-plugin</artifactId> <version>1.5.2.1</version> <executions> <execution> <id>generate-docs</id> <phase>prepare-package</phase> <goals> <goal>process-asciidoc</goal> </goals> <configuration> <backend>html</backend> <doctype>book</doctype> <attributes> <snippets>${project.build.directory}/generated-snippets</snippets> </attributes> </configuration> </execution> </executions> </plugin> <plugin> <artifactId>maven-resources-plugin</artifactId> <executions> <execution> <id>copy-resources</id> <phase>prepare-package</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>${project.build.outputDirectory}/static/docs</outputDirectory> <resources> <resource> <directory>${project.build.directory}/generated-docs</directory> </resource> </resources> </configuration> </execution> </executions> </plugin> ...

restdocs-mockmvc 디펜더시랑 plugin 을 추가하자.
필자의 파일 경로로 하지 않아도 된다.
필자는 generated-snippets 에 snippets 설정 했다.
여기에 실제 adoc 파일들이 추가된다.

그리고 테스트 케이스를 만들어야 된다.
테스트 케이스는 기존에 필자가 포스팅 한거랑 비슷한대 약간 추가 되는 부분이 있다. 살펴보자

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SpringTestApplication.class)
@WebAppConfiguration
@FixMethodOrder(MethodSorters.JVM)
public class AccountControllerTest {

    @Autowired
    private WebApplicationContext webApplicationContext;

    MockMvc mockMvc;


    @Autowired
    private ObjectMapper objectMapper;


    @Rule
    public RestDocumentation restDocumentation =
            new RestDocumentation("target/generated-snippets");

    @Before
    public void before() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext)
                .apply(documentationConfiguration(this.restDocumentation))
//                .alwaysDo(document("{method-name}/{step}/"))
                .alwaysDo(document("{class-name}/{method-name}/"))
                .build();
    }

    @Test
    public void getAccounts() throws Exception {
        this.mockMvc.perform(get("/accounts").accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.[0].name", is("wonwoo")));
    }

    @Test
    public void getAccount() throws Exception {
        this.mockMvc.perform(get("/account/{id}", 1).accept(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name", is("wonwoo")));
    }

    @Test
    public void createAccount() throws Exception {
        Account account = new Account();
        account.setName("wonwoo123");
        this.mockMvc.perform(post("/account").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(account)))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name",is("wonwoo123")));
    }
}

나머진 일반 테스트 케이스처럼 작성하면 되고 before 부분만 변경 되었다.
before의 alwaysDo 메소드에 있는 class-name과 method-name은 위에서 말했던 generated-snippets/* 폴더 아래에 classname/methodname/
형식으로 추가된다.
작성 후에 테스트를 실행 하여 보자. 만약 성공 되었다면 target/generated-snippets/ 아래 폴더에 파일들이 생성 되어있는걸 볼 수 있다.

그리고 나서 src/main/asciidoc/index.adoc 를 추가하자. (필자 기준)

= RESTful Notes API Guide
Andy Wilkinson;
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 4
:sectlinks:

[[overview]]
= Overview

[[overview-http-verbs]]
== HTTP verbs

RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its
use of HTTP verbs.

|===
| Verb | Usage

| `GET`
| Used to retrieve a resource

| `POST`
| Used to create a new resource

| `PATCH`
| Used to update an existing resource, including partial updates

| `DELETE`
| Used to delete an existing resource
|===

[[overview-http-status-codes]]
== HTTP status codes

RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its
use of HTTP status codes.

|===
| Status code | Usage

| `200 OK`
| The request completed successfully

| `201 Created`
| A new resource has been created successfully. The resource's URI is available from the response's
`Location` header

| `204 No Content`
| An update to an existing resource has been applied successfully

| `400 Bad Request`
| The request was malformed. The response body will include an error providing further information

| `404 Not Found`
| The requested resource did not exist
|===


== 사용자 리스트 조회 [get]

사용자를조회


include::{snippets}/account-controller-test/get-accounts/curl-request.adoc[]

=== 요청 구조

==== 요청 파라미터들

include::{snippets}/account-controller-test/get-accounts/http-request.adoc[]

=== 응답 구조

==== 응답 파라미터들

include::{snippets}/account-controller-test/get-accounts/http-response.adoc[]



== 사용자 조회 [get]

사용자를조회


include::{snippets}/account-controller-test/get-account/curl-request.adoc[]

=== 요청 구조

==== 요청 파라미터들

include::{snippets}/account-controller-test/get-account/http-request.adoc[]

=== 응답 구조

==== 응답 파라미터들

include::{snippets}/account-controller-test/get-account/http-response.adoc[]


== 사용자 입력 [post]

사용자를조회


include::{snippets}/account-controller-test/create-account/curl-request.adoc[]

=== 요청 구조

==== 요청 파라미터들

include::{snippets}/account-controller-test/create-account/http-request.adoc[]

=== 응답 구조

==== 응답 파라미터들

include::{snippets}/account-controller-test/create-account/http-response.adoc[]


윗 부분은 spring에서 퍼온거라.. asciidoc이라는 문법이다.
asciidoc-syntax-quick-reference 문법은 여기서 참고.

== 사용자 리스트 조회 [get] 이 부분 부터가 필자가 만들었다. 이쁘진 않지만..
나머지는 그냥 다 문법이고 추가 해야 되는 부분이 있는데 include하는 이 부분이다.
위에서 말했던 파일들을 include 하는 부분이다. 저기에 각각의 페이지가 include 된다.

실제 파일들이 어떻게 생겼는지 보자.
curl이 작성 되어있다. 파일명은 curl-request.adoc로 떨궈진다.

[source,bash]
----
$ curl 'http://localhost:8080/accounts' -i -H 'Accept: application/json'
----

다음으로 reqeust 정보이다. 실제 rest api 의 method, resource, message 정보들이 적혀있다. (물론 지금은 메시지가 없어서)

[source,http]
----
GET /accounts HTTP/1.1
Accept: application/json
Host: localhost

----

마지막으로 response 정보이다. 실제 서버에서 내려 받은 정보를 보여준다.

[source,http]
----
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Content-Length: 81

[ {
  "id" : 1,
  "name" : "wonwoo"
}, {
  "id" : 2,
  "name" : "kevin"
} ]
----

메이븐 기준으로 되어있으니 메이븐을 실행해보자.

mvn install

그러면 target/generated-docs 아래 index.html 파일이 생성 되어 있을 것이다.
한번 열어 보자! 그럼 깔끔한 api 문서가 만들어져있다.

spring-doc

잘 실행되었다면 위와 같은 화면을 볼 수 있을 것이다.

spring @bean

spring @Bean

스프링에 자주 사용되는 어노테이션으로 @Bean에 대해 살짝 맛만 볼라고 한다.
저번에 한번 얘기를 했는데 ConfigurationClassParser클래스 doProcessConfigurationClass 메소드에 여러 메타 어노테이션을 파싱하는 부분이 있다.

...

// Process individual @Bean methods
Set<MethodMetadata> beanMethods = sourceClass.getMetadata().getAnnotatedMethods(Bean.class.getName());
for (MethodMetadata methodMetadata : beanMethods) {
    configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}

...

doProcessConfigurationClass 메소드의 Bean을 파싱하는 부분이다.
하지만 메타 정보만 갖고 있고 이때 인스턴스는 하지 않는다.(이 부분은 저번에도 얘기 한듯 하다)
그럼 우리가 흔히 쓰는 @Bean 은 언제 인스턴스 하는지 알아보자

@Bean
public ModelClass modelClass(){
    return new ModelClass();
}

예로 위와 같은 빈이 있다 가정하자
저번에 얘기 했듯이 Spring은 내부적으로 getBean을 호출하면서 그때 인스턴스도 같이 한다.(물론 안하는 Context도 있다. 안한다는 것보다 할 필요가 없는 듯 해서 그런거 같다.)
그 중에 @Bean 어노테이션들은 BeanMethodInterceptor aop 프록시를 통해 해당 빈을 호출한다.
그리고 나서 DefaultSingletonBeanRegistry클래스에 싱글톤 빈이라고 저장해둔다.(아마도 여기에 모든 싱글톤이 있는 듯하다)

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

저기에는 실제 인스턴스화된 빈들이 저장 되어 있는 곳이다.
왜 이얘길 하냐면 우리들이 흔히 쓰는 getBean을 호출 할때 singletonObjects 멤버 변수에서 꺼내서 주는 거다.

Object sharedInstance = getSingleton(beanName);

getBean을 호출 할때 저 함수를 호출한다. getSingleton 함수를 보자

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
            if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return (singletonObject != NULL_OBJECT ? singletonObject : null);
}

첫 줄만 봐도 알 것이다. Map으로 저장된 빈들을 꺼내서 사용한다.

그럼 Bean의 스코프가 prototype 일 경우는 어떠할까?
실질적으로 하는 행동은 같다.
AbstractBeanFactory 클래스의 일부분이다.

if (mbd.isSingleton()) {
    sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
        @Override
        public Object getObject() throws BeansException {
            try {
                return createBean(beanName, mbd, args);
            }
            catch (BeansException ex) {
                // Explicitly remove instance from singleton cache: It might have been put there
                // eagerly by the creation process, to allow for circular reference resolution.
                // Also remove any beans that received a temporary reference to the bean.
                destroySingleton(beanName);
                throw ex;
            }
        }
    });
    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

else if (mbd.isPrototype()) {
    // It's a prototype -> create a new instance.
    Object prototypeInstance = null;
    try {
        beforePrototypeCreation(beanName);
        prototypeInstance = createBean(beanName, mbd, args);
    }
    finally {
        afterPrototypeCreation(beanName);
    }
    bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}

코드만 봐도 비슷한 일을 하고 있다 대신 싱글톤일 경우엔 getSingleton 을 호출 하면서 싱글톤으로 등록 할 뿐이다.
저기 위에서 말했듯이 getBean을 호출 할 경우 getSingleton에서 싱글톤이 있는지 없는지 확인을 하는데 prototype일 경우엔 없으니 계속 Bean을 새로 호출 할 것이다.

스코프가 session이거나 request일 경우에도 비슷하게 동작 하지 않나 싶다. (확인을 안해서 믿지 마시길)

다 아는 내용 이겠지만 테스트를 해봤다.

  • singleton 일 경우
ModelClass hello = applicationContext.getBean("modelClass", ModelClass.class);
ModelClass hello2 = applicationContext.getBean("modelClass", ModelClass.class);
System.out.println(hello == hello2);
//true
  • prototype 일 경우
ModelClass hello = applicationContext.getBean("modelClass", ModelClass.class);
ModelClass hello2 = applicationContext.getBean("modelClass", ModelClass.class);
System.out.println(hello == hello2);
//false

이렇게 @Bean이 어떻게 동작하는지 조금 알아봤다.

spring boot logging 설정

spring boot logging 설정

저번에는 spring logging에 대해서 알아봤다.
이번에는 설정에 대해 알아볼려고 한다.
아래는 boot의 기본 설정으로 되어있는 로그 포맷이다.

2016-03-15 12:31:52.479  INFO 602 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-03-15 12:31:52.480  INFO 602 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-03-15 12:31:52.573  INFO 602 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-03-15 12:31:52.767  INFO 602 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2016-03-15 12:31:52.891  INFO 602 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2016-03-15 12:31:52.898  INFO 602 --- [           main] ication$$EnhancerBySpringCGLIB$$3dbc3010 : test 
2016-03-15 12:31:52.901  INFO 602 --- [           main] com.example.DemoApplication              : Started DemoApplication in 7.351 seconds (JVM running for 8.53)

첫 번째는 날짜와 시간
두 번째는 로그레벨
세 번째는 프로세스 ID
네 번째는 구분자
다섯 번째는 스레드 명
여섯 번째는 로그네임(클래스 네임) 잘 릴수도 있다 org.springframework.xxxxxx.xxxxxxxxxx.xxxx = o.s.x.x.x
마지막으로 로그메시지다.

스프링 부트의 기본적인 로그 레벨은 info이다.
만약 어플리케이션을 실행 할때 바꾸고 싶다면

java -jar app.jar --debug

이렇게 디버그 레벨로 가능하다.
혹은 설정 자체를 하고 싶다면 application.properties이나 application.yml 파일에 다음과 설정해도 된다.

debug: true

전부다 디버그가 아니라 특정 패키지에만 지정 하고 싶다면 이렇게 하면 된다.
logging.level.* (패키지명) : debug

logging.level.org.springframework.web=DEBUG
logging.level.org.hibernate= ERROR
com.example=ERROR

다음은 로그 파일에 대해 알아보자!
손쉽게 로그파일로 만들수 있다
logging.pathlogging.file 프로파티로 설정 가능하다.
logging.path는 경로를 지정하면 된다. 파일명은 spring.log로 생성된다.
logging.file은 파일 명을 설정해주면 된다. 디폴트는 루트에 저장된다.
두개 같이 쓰면 파일을 먼저 선택하는 듯 하다.
logging.pattern.console 과 logging.pattern.file 로 패턴을 지정 할 수 있다.
DefaultLogbackConfiguration 클래스를 보면 다음과 같이 설정 되어 있다.

private static final String CONSOLE_LOG_PATTERN = "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} "
        + "%clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} "
        + "%clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} "
        + "%clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}";

private static final String FILE_LOG_PATTERN = "%d{yyyy-MM-dd HH:mm:ss.SSS} "
        + "${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}";

만약에 ansi 터미널을 지원한다면 콘솔에 색상을 추가 할 수 있다.
인텔리j는 기본으로 되는듯 하다. 이클립스는 플러그인을 깔면 되는거 같은데 테스트를 해보지 않았다.

spring.output.ansi.enabled=always

spring-boot-ansi

이번에는 커스텀한 로그를 찍 을 수 있다.
classpath에 logback.xml 혹은 logback-spring.xml에 설정을 해두면 boot가 classpath에 있는 파일을 끌고 올라간다.
문서에는 .groovy도 된다고 한다. 하지만 잘모르기때문에 넘어간다.ㅎㅎㅎ
문서에보면 다음과 같이 있다.

로그 시스템 사용자 정의 파일
Logback logback-spring.xml, logback-spring.groovy, logback.xml or logback.groovy
Log4j log4j-spring.properties, log4j-spring.xml, log4j.properties or log4j.xml
Log4j2 log4j2-spring.xml or log4j2.xml
JDK (Java Util Logging) logging.properties

만약 커스텀한 로그파일의 위치를 변경하고 싶다면 logging.config 프로퍼티를 사용하면 된다.
예를 들어 /resources/logback/logback.xml 에 위치 하고 싶다면

logging.config=classpath:logback/logback.xml

다음과 같이 설정 할 수 있다.

spring boot logging 을 참고하여 이글을 작성하였다.