phantomjs에는 viewport 조절 기능이 있지만,

기본 chrome headless에서 없다. 따라서 아래와 같이 nodejs를 개발해야 한다.


https://medium.com/@dschnr/using-headless-chrome-as-an-automated-screenshot-tool-4b07dffba79a


참고로 python쪽을 찾아보려 했지만.. 없다.

(chrome의 remote api인 cproto 라는 게 있지만 더 이상 개발되지 않아서 쓸모가 없다. )


nodejs로 개발해야 할 듯하다. 



Posted by '김용환'
,


Java/Spring에서 이미지를 다운로드하는 예시이다.

byte[]로 다운받고 java.nio.file.Files, Paths를 사용해 다운로드받는다. 


@Service
public class Downloader {

@Autowired
private RestTemplate restTemplate;

public void download(String url) throws Exception {

byte[] binary = restTemplate.getForObject(url, byte[].class);
String fileformat = String.format("image.jpg");
Files.write(Paths.get(fileformat), binary);
}

}


Posted by '김용환'
,


phantomjs 가 2018년 4월에 멈춰졌기로 확인해봤다니..


기존의 고생한 것도 있고. headless chrome 때문에 옮겼다고 한다.


https://groups.google.com/forum/#!topic/phantomjs/9aI5d-LDuNE


Hi,


I want to make an announcement.


I think people will switch to it, eventually. Chrome is faster and more stable than PhantomJS. And it doesn't eat memory like crazy.

I don't see any future in developing PhantomJS. Developing PhantomJS 2 and 2.5 as a single developer is a bloody hell.
Even with recently released 2.5 Beta version with new and shiny QtWebKit, I can't physically support all 3 platforms at once (I even bought the Mac for that!). We have no support.
From now, I am stepping down as maintainer. If someone wants to continue - feel free to reach me.

I want to give credits to Ariya, James and Ivan! It was the pleasure to work with you. Cheers!
I also want to say thanks to all people who supported and tried to help us. Thank you!

With regards,

Vitaly.


Posted by '김용환'
,

gradle 에러 내용은 다음과 같다. 



난 여태 gradle은 순서에 상관없는 빌드를 지원하는 것으로 알고 있었다.

그런데 아래와 같은 에러가 발생한다. 


아래와 같이 사용했더니


repositories {
mavenCentral()
}

plugins {
id 'org.springframework.boot' version '2.1.6.RELEASE'
id 'java'
}



아래와 같은 gradle 에러가 발생한다.

only buildscript {} and other plugins {} script blocks are allowed before plugins {} blocks, no other statements are allowed




https://docs.gradle.org/5.4.1/userguide/plugins.html 문서를 참고하면.

plugins (플러그인 바이너리) 스펙에 constraint를 설명하고 있다. 


에러 문구 대로 plugins {} 구문의 앞에는 buildscript {}와 기타 plugin{}만 사용될 수 있다. 





Posted by '김용환'
,


인터넷에 많이 공개된 Spring Rest Docs를 공부하고 적용해봤다.



* Spring Rest Docs가 Swagger2에 비해 갖는 장점

- 내가 원하는 형태로 문서화할 수 있다. (대신 단점도 됨)

- 테스트를 무조건 만들어야 하고 문서화를 진행해야 한다. (안전)

- Spring Controller의 지저분한 annotation이 깔끔해짐 (코드의 간결성)

- Swagger2는 2018년 중반 이후부터 더 이상 개발되지 않고 있다. 



* Spring Rest Docs가 Swagger2에 비해 갖는 단점

- 처음에 손이 많이 간다. 

  asciidoc로 문서화를 수행해야 한다. 처음에는 디폴트가 먼저도 모름. 헤맴. 그리고 만들어진 html 문서를 resource static 문서로 복사해야 한다. gradle, maven 자동화가 필요하다.

- UI를 못하거나 문서화를 자동으로 해주는 swagger2가 적당한 선이 아직까지는 내게 어울림

- swagger2의 try it out 기능이 없는 것 같다(또는 어디에 있는지 못 찾겠다.) 

   swagger2의 try it out은 api 연동의 핵심이라.. 의사소통을 많이 줄여준다. 


-> (개인적으로) 아직은 Swagger2가 훨씬 편한 듯 하다. 그러나 Spring Rest Docs의 철학이 아무 매력적이다. '테스트 없는 문서화가 의미가 있나?' 테스트를 강제하는 형태가 좋아보였다. 



Spring Rest Docs를 사용하면 아래와 비슷하게 나온다. 귀찮아서 정리를 못했지만 손이 조금 간다. 꼼꼼한 사람들에게는 좋을 것 같다. 







나머지는 코드이다. 


https://github.com/knight76/springboot2-restdocs


build.gradle


buildscript {
repositories {
mavenCentral()
jcenter()
}

dependencies {
classpath 'org.asciidoctor:asciidoctor-gradle-plugin:1.5.9'
}
}


plugins {
id 'org.springframework.boot' version '2.1.6.RELEASE'
id "org.asciidoctor.convert" version "1.5.9"
}

apply plugin: 'org.asciidoctor.convert'
apply plugin : 'java'
apply plugin : 'io.spring.dependency-management'
apply plugin : 'org.asciidoctor.convert'

ext {
lombokVersion = "1.18.8"
swagger2Version = '2.9.2'
oldSwagger2Version = '1.5.21'
}


group = 'com.google.knight76'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'


ext {
snippetsDir = file('build/generated-snippets')
}

test {
outputs.dir snippetsDir
}

asciidoctor {
inputs.dir snippetsDir
dependsOn test
}

bootJar {
dependsOn asciidoctor
from ("${asciidoctor.outputDir}/html5") {
into 'static'
}
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation('org.springframework.boot:spring-boot-starter-web') {
exclude module: "spring-boot-starter-tomcat"
}
implementation "org.projectlombok:lombok:${lombokVersion}"
implementation 'org.springframework.boot:spring-boot-starter-undertow'

asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation('org.springframework.restdocs:spring-restdocs-mockmvc')
}


src/doc/asciidoc/api-docs.adoc

:sectnums:
:sectnumlevels: 5
:toc: left
:toclevels: 3
:page-layout: docs


= Hello World

== Hello World (Plain)

include::{snippets}/hello-world-test/hello-world/http-request.adoc[]
include::{snippets}/hello-world-test/hello-world/curl-request.adoc[]
include::{snippets}/hello-world-test/hello-world/http-response.adoc[]
include::{snippets}/hello-world-test/hello-world/request-body.adoc[]
include::{snippets}/hello-world-test/hello-world/response-body.adoc[]
include::{snippets}/hello-world-test/hello-world/httpie-request.adoc[]


== Hello World (Json)

include::{snippets}/hello-world-test/hello-world-json/http-request.adoc[]
include::{snippets}/hello-world-test/hello-world-json/curl-request.adoc[]
include::{snippets}/hello-world-test/hello-world-json/http-response.adoc[]
include::{snippets}/hello-world-test/hello-world-json/request-body.adoc[]
include::{snippets}/hello-world-test/hello-world-json/response-body.adoc[]
include::{snippets}/hello-world-test/hello-world-json/response-fields.adoc[]
include::{snippets}/hello-world-test/hello-world-json/httpie-request.adoc[]
include::{snippets}/hello-world-test/hello-world-json/request-parameters.adoc[]

== City (add-city)

include::{snippets}/cities-controller-test/add-city/http-request.adoc[]
include::{snippets}/cities-controller-test/add-city/curl-request.adoc[]
include::{snippets}/cities-controller-test/add-city/http-response.adoc[]
include::{snippets}/cities-controller-test/add-city/request-body.adoc[]
include::{snippets}/cities-controller-test/add-city/response-body.adoc[]
include::{snippets}/cities-controller-test/add-city/response-fields.adoc[]


== City (delete-city)

include::{snippets}/cities-controller-test/delete-city/http-request.adoc[]
include::{snippets}/cities-controller-test/delete-city/curl-request.adoc[]
include::{snippets}/cities-controller-test/delete-city/http-response.adoc[]
include::{snippets}/cities-controller-test/delete-city/request-body.adoc[]
include::{snippets}/cities-controller-test/delete-city/response-body.adoc[]
include::{snippets}/cities-controller-test/delete-city/response-fields.adoc[]

== City (get-cities)

include::{snippets}/cities-controller-test/get-cities/http-request.adoc[]
include::{snippets}/cities-controller-test/get-cities/curl-request.adoc[]
include::{snippets}/cities-controller-test/get-cities/http-response.adoc[]
include::{snippets}/cities-controller-test/get-cities/request-body.adoc[]
include::{snippets}/cities-controller-test/get-cities/response-body.adoc[]
include::{snippets}/cities-controller-test/get-cities/response-fields.adoc[]


== City (get-city)

include::{snippets}/cities-controller-test/get-city/http-request.adoc[]
include::{snippets}/cities-controller-test/get-city/curl-request.adoc[]
include::{snippets}/cities-controller-test/get-city/http-response.adoc[]
include::{snippets}/cities-controller-test/get-city/request-body.adoc[]
include::{snippets}/cities-controller-test/get-city/response-body.adoc[]
include::{snippets}/cities-controller-test/get-city/path-parameters.adoc[]
include::{snippets}/cities-controller-test/get-city/response-fields.adoc[]


예시

@Test
public void getCity() throws Exception {
// given
String response = "{'id': 1,'name':'Bratislava','population':432000}";

// when
when(cityService.get(1L)).thenReturn( Optional.of(new City(1L, "Bratislava", 432000)));

ResultActions result =
mockMvc.perform(get("/api/1/city/{id}", 1L)
.contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
.andDo(print());

// then
result.andExpect(status().isOk())
.andDo(document.document(
pathParameters(
parameterWithName("id").description("도시의 id")),
responseFields(
fieldWithPath("id").description("도시의 id"),
fieldWithPath("name").description("도시 이름"),
fieldWithPath("population").description("도시 인구")
)))
.andExpect(jsonPath("id", is(notNullValue())))
.andExpect(content().json(response));
}






참고 

http://woowabros.github.io/experience/2018/12/28/spring-rest-docs.html

https://docs.spring.io/spring-restdocs/docs/current/reference/html5/


Posted by '김용환'
,


보통은 org.springframework.web.HttpMediaTypeNotSupportedException 예외는


Rest api에서 허용할 수 있는 MediaType이 아니면 에러가 난다.

@RequestMapping(path="/api/1", 
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, 
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)


ResultActions result =
mockMvc.perform(post("/api/1/city")
.contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
.content(requestJson))
.andDo(print());




그러나, 특별한 경우로 

SpringBoot의 UI 모델 또는 DTO에  lombok, Jackson의 JsonCreator가 잘못 결합될 때. 이상한 에러가 난다.

그래서 몇시간동안 삽질, 주화 입마에 빠질 수 있다.    

org.springframework.web.HttpMediaTypeNotSupportedException






문제가 되는 코드는 다음과 같다.


@RestController

@RequestMapping("/api/test")

public class CityController {


    @PostMapping

    public Product post(@RequestBody City city) {

.....

    }


}




@Data

public class City {

@NonNull

private Long id;


@NonNull

private String name;


@NonNull

private Integer population;


@JsonCreator

public City(Long id, String name, Integer population) {

this.id = id;

this.name = name;

this.population =  population;

}

}


또는 아래와 같이 AllArgsConstructor에 JsonCreator를 사용하면 안된다.


@Data

@Builder

@AllArgsConstructor(onConstructor = @__(@JsonCreator))

public class City {


@NonNull


private Long id;




@NonNull


private String name;




@NonNull


private Integer population;

}


HttpMediaTypeNotSupportedException가 발생한다.



따라서  아래와 같이 모델을 변경하니 잘 동작한다.

@Data

@AllArgsConstructor

@NoArgsConstructor

public class City {

@NonNull

private Long id;


@NonNull

private String name;


@NonNull

private Integer population;

}




이미 보고는 되어 있는데..  해결 방안은  딱히 없으니. 알아서 잘 피해야 할 것 같다. 



https://github.com/FasterXML/jackson-databind/issues/1239

https://github.com/spring-projects/spring-boot/issues/12568


Posted by '김용환'
,


에러 

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class com.example.model.City]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.example.model.City` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)

 at [Source: (PushbackInputStream); line: 2, column: 3]

 


생성자에 Jackson의 @JsonCreator를 붙여 해결하고 싶겠지만. 결국 문제가 발생할 것이다. 




 @Data

public class City {

@NonNull

private Long id;


@NonNull

private String name;


@NonNull

private Integer population;


@JsonCreator

public City(Long id, String name, Integer population) {

this.id = id;

this.name = name;

this.population =  population;

}

}


 

아래 org.springframework.web.HttpMediaTypeNotSupportedException 이 발생하게 된다...

https://knight76.tistory.com/entry/orgspringframeworkwebHttpMediaTypeNotSupportedException-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0


최대한 Lombok을 잘 사용하는 것이 좋다. 

Posted by '김용환'
,



sprin test를 사용하다가 로그에 request의 body(특정 json)가 안나오는 문제가 있다. 


    

    MockHttpServletRequest:

      HTTP Method = POST

      Request URI = /api/1/city/

       Parameters = {}

          Headers = [Accept:"application/json"]

             Body = <no character encoding set>

    Session Attrs = {}

    


그래서 코드에 characterEncoding을 추가했더니 문제가 발생하지 않았다. 


    ResultActions result =

mockMvc.perform(post("/api/1/city/", 10L)

                .accept(MediaType.APPLICATION_JSON)

                .characterEncoding("utf-8")

                .content(requestJson))

       .andDo(print());

       

       

    

    

    다음과 같이 request의 body 정보가 출력되었다.

    MockHttpServletRequest:

      HTTP Method = POST

      Request URI = /api/1/city/

       Parameters = {}

          Headers = [Accept:"application/json"]

             Body = {

  "id" : 10,

  "name" : "Bratislava",

  "population" : 432000

}

    Session Attrs = {}

    



Posted by '김용환'
,


gradle, asciidoctor를 사용할 때 

maven plugin 예제를 참고하다 잘  안되는 경우가 있다.


(디폴트로) maven의 adoc 파일을 생성하는 위치는 src/main/asciidoc 인 반면,

gradle 의 경우는 src/docs/asciidoc 이다.


gradle 사용자는 주의해야 한다. 




Posted by '김용환'
,



Proxy를 사용해야 하는 환경에서

grails 2.x 컴파일을 하는데, 제대로 inhouse library를 다운받지 못한 현상이 발생했다.

마치 nonProxy 설정에 문제가 있다는 느낌..



grails add-proxy client --host=${PROXY_HOST} --port=${PROXY_PORT} --noproxy="${JAVA_NO_PROXY}"

grails set-proxy client

grails compile 



grails dependency-report를 실행해보면, inhouse lib을 못가져온다. 



원인은 setproxy 하면서 생성된 .grails/ProxySettings.groovy 파일을 삭제하고

grails compile 을  실행하니 제대로 동작한다.



Posted by '김용환'
,