flowerspring

spring boot를 공부하면서 설정에 관련된 사항을 정리 해본다.


1. server 포트 변경

#80 포트로 변경

server.port=80


#랜덤 포트로 변경

server.port=0




관련사이트 :

http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html

Comment +0

자바 웹프로젝트를 진행하면서 소스 수정이 일어날 때마다 바로 반영이 되지 않기 때문에 

톰캣을 재기동하거나, 자동으로 소스를 반영하고 톰캣을 재실행되도록 설정을 한다.


이는 너무나 일상적인 개발 방법이라 전혀 이상하지 않았지만 매우 불편하고 시간을 허비하는 일이다.

순수 JSP만 이용하여 개발을 하게 되면 이런 과정을 거치지 않겠지만 요즘은 이런 개발은 하지 않을뿐더러 객체지향적으로 설계를 하다보니

JAVA 파일 갯수는 어마하게 늘어났다. 

파일 하나 수정했을 뿐인데 톰캣을 재기동하고, 때로는 파일 하나 수정했을 뿐인데 설정에 의해서 톰캣이 자동으로 재기동 된다.


이런 과정을 줄여주는 도구가 여러가지가 있겠지만 예전에는 JRebel을 잠깐 사용했었다.

좋은 점이 있지만 무엇보다 유로로 제공되고 잠깐 동안 커뮤니티 버전을 사용했는데 지금은 제공하지 않는지 URL을 찾을수가 없었다.


이번에는 이런  기능을 무료로 제공해주는 도구가 있어서 설정 방법을 요약한다.      

이름은 spring-loaded이며 다운로드는 아래 링크를 통하여 다운로드 받을 수 있다.


- homepage : https://github.com/spring-projects/spring-loaded

- maven : http://mvnrepository.com/artifact/org.springframework/springloaded/1.2.6.RELEASE


먼저 사용을 하기 위해서는 해당 사이트에서 파일을 다운로드 한다.

파일명은 현재 기준 가장 최근 파일이 아래와 같다.


springloaded-1.2.6.RELEASE.jar


해당 파일만 다운로드 받으면 준비가 끝난다.

제법 간단하다.


파일을 다운로드 받았다면 이클립스 톰캣 서버 설정을 일부 변경해줘야 한다.

이클립스에서 해당 웹어플리케이션의 톰캣 서버를 더블 클릭하면 톰캣 설정이 화면이 뜨게 되는데

1. Publishing -> Automatically publish when resources change 항목 체크

2. Server Options 탭의 모든 체크 항목 해제

3. General Information 탭 항목의 Open launch configuration 텍스트를 클릭한다.




4. Open launch configuration 텍스트를 선택하게 되면 해당 창에서 Arguments탭을 선택하여 Vm arguments 창에 위 이미지처럼 다음 옵션을 추가해준다.


-javaagent:/Users/hmjkor/Downloads/springloaded-1.2.6.RELEASE.jar -noverify


위에서 /Users/hmjkor/Downloads 부분은 각자의 PC 환경에 따라 다르게 지정이 된다. 

springloaded-1.2.6.RELEASE.jar 파일이 저장되어 있는 경로로 지정한다.  


모든 설정이 끝나면 OK 버튼을 눌러 설정을 종료하고 서버 설정 화면에서 하단 modules 탭으로 들어가서 해당 웹 어플리케이션을 선택한 후 Editor 버튼을 선택한다.




5. Editor 버튼을 통해서 띄우진 Editor Web Module 창에서 Auto reloading enabled 체크 항목을 해제한 후 OK 버튼을 눌러 설정을 종료한다.


이로써 모든 설정은 끝났으며 java 파일을 작성한 후 테스트 해본다.




데몬 프로그램에서는 다음과 같은 형태로 실행한다.


java -javaagent:<pathTo>/springloaded-{VERSION}.jar -noverify SomeJavaClass 



springloaded-1.2.6.RELEASE.jar

springloaded-1.2.4.RELEASE.jar

springloaded-1.2.7.RELEASE.jar






6. intellij 일 경우도 방법은 비슷하다. Edit Configurations 메뉴에서 톰갯을 등록해준 후 VM options 란에 옵션을 4.절에와 같이 추가해주면 된다.



그리고 On 'Update' action, On frame deactivation 옵션을 Update classes and resources 항목을 선택해주면 된다.

Update classes and resources 항목이 보이지 않는다면 Deployment 탭으로 이동한 후  Deploy at the server startup 항목의 배포 형식이 :exploded로 설정한다.





참고사이트 : 

https://github.com/spring-projects/spring-loaded



Comment +1

네이버 지도 API를 사용하여 개발한 사이트가 있는데 해당 사이트는 https로 되어 있습니다.

하지만 지도를 보여주는 메뉴를 선택하게 되면 지도는 표시 되지 않으며 크롬 주소창 끝쪽에 방패모양의 아이콘만 표시되며 지도는 표시가 되지 않습니다.

해당 증상 및 해결방법은 아래와 같습니다.


크롬에서는 https로 연결시 http 컨텐츠를 차단한다고 합니다.

이 옵션을 해제할 수 있는 기능은 별도로 제공되어 있지 않으며, 크롬 구동시 다음 옵션을 주면 http 연결된 컨텐츠도 다운로드 합니다.


-allow-running-insecure-content





증상 

'이 페이지가 인증되지 않은 소스에서 스크립트를 로드하려고 시도하고 있습니다.'라는 오류가 표시되는 이유는 Chrome에서 안전하지 않은 사이트로부터 사용자를 보호하고 페이지에 표시된 사용자 정보가 악용되지 않도록 하기 위해서입니다. 이 경우 검색주소창에 방패  가 표시됩니다.


알림 무시

권장되지는 않지만 원하는 경우 안전하지 않은 스크립트 로드를 클릭하여 페이지의 알림을 무시할 수 있습니다. 그럼 Chrome이 페이지를 새로고침하여 안전하지 않은 콘텐츠를 포함한 모든 콘텐츠를 로드하게 됩니다. 해당 페이지가 안전하지 않다고 표시하기 위해 상단의 검색주소창의 https에 빨간색 줄 이 표시됩니다.

특정 웹 콘텐츠 차단

설정 페이지에서 자바스크립트나 이미지 등 특정 유형의 웹 콘텐츠가 모든 사이트에 표시되지 않도록 선택할 수 있습니다. 자세한 내용은 웹 콘텐츠 설정 조정을 참조하세요.

사이트의 문제 스크립트 찾기

웹 개발자인 경우 사이트에서 발견된 안전하지 않은 스크립트를 확인할 수 있습니다. 상단 툴바에서 보기 > 개발자 > 자바스크립트 콘솔을 클릭하세요. 안전하지 않은 코드나 유해한 확장 프로그램이 문제의 원인일 수 있습니다.


참고 : https://support.google.com/chrome/answer/1342714?hl=ko


Comment +0

AppData 폴더에는 앱 설정, 파일 및 PC의 앱 관련 데이터가 포함되어 있습니다. 기본적으로 이 파일은 파일 탐색기에서 숨겨져 있으며 Local, LocalLow, Roaming이라는 세 개의 숨겨진 하위 폴더를 가지고 있습니다.

  • Roaming. 이 폴더(%appdata%)에는 사용자가 도메인에 있는 경우 등 PC 간에 사용자 프로필로 이동할 수 있는 데이터가 포함되어 있습니다. 이 데이터에는 서버와 동기화할 수 있는 기능이 있기 때문입니다. 예를 들어 도메인에서 다른 PC에 로그인하는 경우 사용자 웹 브라우저의 즐겨찾기 또는 책갈피를 사용할 수 있습니다.

  • Local. 이 폴더(%localappdata%)에는 사용자 프로필로 이동할 수 없는 데이터가 포함되어 있습니다. 일반적으로 이 데이터는 PC와 관련되어 있거나 서버와 동기화하기에 너무 큽니다. 예를 들어 보통 웹 브라우저는 여기에 임시 파일을 저장합니다.

  • LocalLow. 이 폴더(%appdata%/…/locallow)에는 이동할 수 없는 데이터가 포함되어 있지만 액세스 권한 수준이 더 낮습니다. 예를 들어 보호 모드나 안전 모드에서 웹 브라우저를 실행 중인 경우 앱에서는 LocalLow 폴더의 데이터에만 액세스할 수 있습니다.

Local, LocalLow 또는 Roaming 폴더 중 앱 정보를 저장할 위치는 앱에서 선택합니다. 대부분의 데스크톱 앱은 기본적으로 Roaming 폴더를 사용하지만 대부분의 Windows 스토어 앱은 기본적으로 Local 폴더를 사용합니다

'DEVELOPMENT' 카테고리의 다른 글

로컬 jar 파일 maven porject에 추가하는 법  (0) 2017.04.25
hsqldb 사용하기  (0) 2016.07.23
windows에 있는 AppData 폴더란?  (0) 2015.07.30
Git  (0) 2013.12.30
dxf file format  (0) 2012.08.16
XSLT 빠른 가이드  (0) 2012.01.13

Comment +0

서버를 운영하다 보면 여러가지 이유때문에 서버가 다운되는 경우가 발생을 하게 됩니다. 대부분의 경우 톰캣 스크립트를 통해 서버를 재시작 시켜주기만 하면 되는 간단한 작업이지만, 휴가를 가거나 오랜기간 컴퓨터앞에 앉아 있을 수가 없게 되었을때 서버 다운이 발생하면 속수무책으로 당할수 밖에 없습니다. 그리고 이런 문제가 자주 발생하게 된다면 사용자들은 해당 서비스를 신뢰할수 없게 될것입니다. 이런 때를 대비해서 쉘 스크립트를 통해 톰켓 서버의 상태를 체크 하여 서버가 다운되었을때 재실행 시키는 스크립트를 작성하고,CRONTAB을 이용하여 주기적으로 서버의 상태를 체크하게 하면 그나마 나은 서비스가 될수 있을 것입니다. 아래의 스크립트 내용은 톰켓의 상태를 체크하여 프로세스가 죽어 있으면 스크립트를 실행하여 서버를 재시작 시키는 스크립트 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
 
export JAVA_HOME=사용자들의 JAVA_HOME
export PATH=$PATH:$JAVA_HOME/bin
export CATALINA_HOME=톰켓 설치 위치
export PATH=$PATH:$CATALINA_HOME/bin
 
if [ -z "`ps -eaf | grep java| grep tomcat`" ]; then
        $CATALINA_HOME'/bin/startup.sh'
        END_TIME=`date +%m/%d' '%H:%M:%S`
        echo "["$END_TIME"] tomcat restart.."
fi

기본적으로 JAVA_HOME같은 환경변수들은 JDK를 설치할때 세팅을 해줌에도 불구하고 스크립트에서 다시 export 시키는 이유는 crontab에서 스크립트를 실행하면 .bash_profile이나 /etc/profile 에 JAVA_HOME, PATH같은 환경변수를 선언해 두었더라도 환경변수를 읽을 수 없어 톰캣실행이 잘 되지 않습니다. 그렇기 때문에 톰켓을 실행시키기 위한 필수 환경변수들을 스크립트에 다시 한번 선언해 주어야 합니다. 이후 해당 스크립트를 CRONTAB에 등록하면 되는데, CRONTAB에 스크립트를 등록하는 방식은 다음과 같습니다.

1
2
3
$ crontab -e
##vi 에디터가 실행되면 다음의 내용을 추가합니다.
*/5 * * * * /스크립트 경로/tomcat_checker.sh >> /data/log/tomcat-restart.log 2>&1

따로 cron expression은 설명하지 않겠습니다. 위의 뜻은 5분마다 tomcat_checker.sh 스크립트를 실행시키고 그에 따른 결과 로그를 tomcat-restart.log에 append(>>) 하겠다는 뜻입니다. 위와 같이 설정한 후 root 권한으로 crontab을 재시작 시키면 crontab이 적용되게 됩니다.

1
2
$ su
# /sbin/service crond restart

그리고 crontab -l 명령어로 해당 crontab에 스케줄러가 잘 등록이 되었는지 확인 하면 됩니다.  


[원문출처] http://krespo.net/160

Comment +0

서버중 외부로 나가는 트래픽이 막혀있는 서버가 있다면 프록시를 통해 외부로 나가는 요청을 할수가 있습니다.


1. 로그인 세션 동안만 유지되는 방법

텔넷이나 ssh에 로그인 되어있는동안만 프록시를 사용하고 있다면 다음과 같이 사용하면 됩니다.

1
2
3
4
5
6
# 프록시 서버 설정
$ export http_proxy=http://www.yourproxyserver.com:port
$ export https_proxy=http://www.yourproxyserver.com:port
#프록시서버 해제
$ unset http_proxy
$ unset https_proxy

2. 로그인 세션이 종료된 후에도 프록시를 유지시키는 방법

1
2
3
4
5
6
7
8
9
# 여기서는 .bashrc에 작업했지만 .profile, .bash_profile, .bashrc_profile 어디에 설정해도 동일합니다.
$ vi ~/.bashrc
 
#아래내용 추가
export http_proxy=http://www.yourproxyserver.com:port
export https_proxy=http://www.yourproxyserver.com:port
# 저장후
 
$source ~/.bashrc

이후 wget이나 curl로 외부로 요청이 되었을때 데이터를 제대로 가져오면 프록시 설정이 제대로 된것입니다.

1
2
3
$ wget www.daum.net
#혹은
$ curl -0 www.daum.net


[원문출처]http://krespo.net/188

Comment +0

오라클에서 데이터를 저장하다 보면 한 테이블 내에 부모 자식 관계(상하위 관계)로 표현하는 데이터가 발생될 때가 있다. 예를 들어

게시판 ↳ 공지사항 게시판 ↳ 사용자 게시판

와 같이 메뉴를 저장하는 테이블이 있다면 상위 게시판이라는 메뉴는 하위에 공지사항 게시판 메뉴, 사용자 게시판 메뉴를 하위로 가진다면, 데이터 구조는 다음과 같이 생성될 것이다.

MENUIDNAMEURLPARENTMENUID
1공지사항 게시판/notice.html2
2게시판/index.htmlnull
3사용자 게시판/user.html2

만약 쿼리를 상위 메뉴 데이터를 먼저 나오게 하고 그 뒤에 하위 메뉴들을 나오게 하려면 어떻게 해야할까? 이때 Oracle에서는 start with, connect by prior를 사용하여 데이터를 셀렉트 할수 있다.

1
2
3
4
5
6
SELECT
    menuid, name, url, parentmenuid
FROM menu
START WITH parentmenuid is null /*상위(부모)의 조건을 명시*/
CONNECT BY PRIOR menuid = parentmenuid  /*상위의 키와 하위의 키를 열결시킴*/
ORDER SIBLINGS BY menuid ASC;   /*하위데이터 정렬 조건*/

이렇게 쿼리를 사용하게 되면 다음과 같이 셀렉팅이 된다.

MENUIDNAMEURLPARENTMENUID
2게시판/index.htmlnull
1공지사항 게시판/notice.html2
3사용자 게시판/user.html2

[원문출처]http://krespo.net/171

Comment +0

Oracle 11g에 LISTAGG라는 function이 추가되었습니다. 이 function은 group by로 그룹핑된 문자열 row들을 하나의 컬럼으로 결합시키는 역할을 하게 됩니다. 간단하게 예를 들어보도록 하겠습니다.



 DEPTNO

 NAME

 1

 김태희

 1

 전지현

 2

 성나정

 3

 고아라 


위의 데이터에서 부서번호(DEPTNO)가 1인 사원들의 이름을 한번에 뽑고 싶을 경우에는 아래와 같은 쿼리를 사용하면 됩니다.

1
2
3
4
5
6
SELECT
    deptno, LISTAGG(name, ',') within group (order by name) name
FROM
    test_tb
where deptno = 1
group by deptno;

LISTAGG(결합시킬 컬럼, ROW 사이의 결합 문자) within group (해당 그룹 사이의 정렬조건)

으로 간단하게 사용할 수 있습니다


위 쿼리를 실행하면 결과값은 아래처럼 나오게 됩니다.


DEPTNO 

NAME 

김태희, 전지현 


[원문출처]http://krespo.net/195

Comment +0




-- 패스워드 기간 확인
select * from dba_profiles where profile = 'DEFAULT';

-> Password life cycle 이 180 이 기본 설정임.

alter profile default limit password_life_time unlimited;
-> 기간제한 없이 변경해 준다.


alter user 계정이름 identified by 패스워드 ;

-> 계정에 대한 패스워드를 다시 설정해 준다.


--계정 잠금

ALTER USER "계정명" ACCOUNT LOCK;

 

--계정 잠금 해제

ALTER USER "계정명" ACCOUNT UNLOCK;

 

--계정 비밀번호 만료

ALTER USER "계정명" PASSWORD EXPIRE;

 

--계정 비밀번호 만료해제 or 변경

ALTER USER "계정명" IDENTIFIED BY "비밀번호";


-- 계정 조회

SELECT USERNAME, ACCOUNT_STATUS, EXPIRY_DATE, PROFILE FROM DBA_USERS

WHERE ACCOUNT_STATUS = 'OPEN';


-- 만료기간 무제한 profile 변경

ALTER PROFILE DEFAULT LIMIT

  FAILED_LOGIN_ATTEMPTS UNLIMITED

  PASSWORD_LIFE_TIME UNLIMITED;


-- 만료기간 제한 profile 변경

ALTER PROFILE DEFAULT LIMIT

  FAILED_LOGIN_ATTEMPTS 20

  PASSWORD_LIFE_TIME 365;

  

--Profile 변경

SQL> ALTER PROFILE default LIMIT

  2  > FAILED_LOGIN_ATTEMPTS 3

  3  > PASSWORD_LIFE_TIME 60

  4  > PASSWORD_GRACE_TIME  10; 

  

-- profile 파라메터 설명

PASSWORD history    : 이전 암호를 기억해 놓았다가 다음에 변경시 동일한 암호사용을 금지함 password_reuse_time : 동일한 password를 적용한 기간동안 사용금지 

password_reuse_max : 입력된 value값만큼만 사용가능한 횟수를 제한 ex: 3이라고 입력하면 3번만 사용가능 password_life_time     : password 생명주기 ex : 30 -> 30일마다 변경해야함 

password_grace_time : password 변경 만료 알림을 value일 전부터 알림 

failed_login_attempts   :password 입력실패시 재시도 가능횟수 최종 실패시 계정 lock걸림 

password_lock_time   : lock걸렸을때 value값만큼 잠겨있음 1일단위임 1/24는 1시간 1/1440은 1분 

password complexity verification(패스워드 복잡성)password설정시 제약조건


-- 파라미터 정보

Parameter: FAILED_LOGIN_ATTEMPTS

Default Setting:  10

Description:  Sets the maximum times a user login is allowed to fail before locking the account.

 

Parameter: PASSWORD_LIFE_TIME

Default Setting:  180

Description:  Sets the number of days the user can use his or her current password.


[출처] http://aspopark.tistory.com/31

Comment +0

java.lang.reflect를 이용하면 우리가 원하는 클래스에 대한 invoke가 가능하다는 것은 알고 있을 것이다.

하지만 classpath에 등록안되어진 클래스들에 대해서는 어떻게 할 것인가? 


일일이 사용자에게 클래스 패스를 설정하게 할수만은 없는 일이다. 


보통의 엔진들을 보게 되면 install되어진 디렉토리의 위치만을 세팅하도록 하고 있다. 

set JAVA_HOME이라던지 

set ANT_HOME이라던지.. 


쉘스크립트에 의하여 그러한 것들을 정의하여 java process를 띄우곤 하는데 그러면 

내가 ant.jar등을 등록하지 않았음에도 불구하고 해당 애플리케이션들이 잘 작동하는 이유는 무엇일까? 

그것은 바로 ClassLoader에 숨겨져 있다. 

아래에서 보여지는 샘플코드는 classpath 프로퍼티에 등록이 되어지지 않은 클래스들에 대한 조작을 할 것이다. 


그렇게 함으로서 이 글을 읽는 당신이 만든 애플리케이션이 별다른 클래스로딩 정책 없이도 작동이 될수 있겠다. 

그러려면 또한 잘 알아야 하는것이 reflection API이거늘... 

이부분에서는 그러한 것을 생략하고 URLClassLoader를 이용하여 디렉토리나 jar파일을 등록하여 가져오는 

방법을 설명하도록 하겠다. 


ClassLoader클래스는 이미 1.0API부터 존재해왔으면 URLClassLoader는 1.2에 새롭게 추가된 클래스이다. 

우리가 사용하는 파일시스템이 URL이란 이름하에 조작이 될 수 있다는 것을 우선 명심하기 바란다. 

왜냐면 file:/// 이란 URI를 사용하기 때문이다. 


아래에서는 특정한 디렉토리 안의 jar파일에 대한 class loading샘플을 보여준다.. 

  1. import java.io.*;
  2. import java.net.*;
  3. public class ClassLoading {
  4.   public static void main(String [] args) throws Exception {
  5.     // Create a File object on the root of the directory containing the class file
  6.     File file = new File("D:/_Develop/jmxSamples/customMBean/log4j-1.2.8.jar");    
  7.     try {
  8.       // Convert File to a URL
  9.       URL url = file.toURL();          // file:/D:/_Develop/jmxSamples/customMBean/log4j-1.2.8.jar
  10.       URL[] urls = new URL[]{ url };
  11.       System.out.println(urls);
  12.        
  13.       // Create a new class loader with the directory
  14.       ClassLoader cl = new URLClassLoader(urls);
  15.       System.out.println(cl);
  16.        
  17.       // Load in the class; Logger.class should be located in
  18.       // the directory file:/D:/_Develop/jmxSamples/customMBean/log4j-1.2.8.jar
  19.       Class cls = cl.loadClass("org.apache.log4j.Logger");
  20.       System.out.println(cls);    
  21.     } catch (MalformedURLException e) {
  22.       e.printStackTrace();
  23.     } catch (ClassNotFoundException e2) {
  24.       e2.printStackTrace();
  25.     }  
  26.   }
  27. }




위에서 보는 것처럼 디렉토리를 설정하거나 특정 jar파일을 사용할 수 있도록 작성한다. 

특정파일이 가르키지 않으면 해당 디렉토리의 class파일들을 package형태로 참조하도록 할 수 있는데 

해당 디렉토리에 대한 클래스 로딩 샘플을 아래와 같다. 

  1. import java.io.*;
  2. import java.net.*;
  3. public class ClassLoading {
  4.   public static void main(String [] args) throws Exception {
  5.     // Create a File object on the root of the directory containing the class file
  6.     File file = new File("D:/_CVSDevelop/jca_hello_adapter/build/classes");
  7.      
  8.     try {
  9.       // Convert File to a URL
  10.       URL url = file.toURL();          // file:/D:/_CVSDevelop/jca_hello_adapter/build
  11.       URL[] urls = new URL[]{ url };
  12.       System.out.println(urls);
  13.        
  14.       // Create a new class loader with the directory
  15.       ClassLoader cl = new URLClassLoader(urls);
  16.       System.out.println(cl);
  17.        
  18.       // Load in the class; Test.class should be located in
  19.       // the directory file:/D:/_CVSDevelop/jca_hello_adapter/build/classes/com/bea/jca/test/Test
  20.       Class cls = cl.loadClass("com.bea.jca.test.Test");
  21.       System.out.println(cls);
  22.      
  23.     } catch (MalformedURLException e) {
  24.       e.printStackTrace();
  25.     } catch (ClassNotFoundException e2) {
  26.       e2.printStackTrace();
  27.     }  
  28.   }
  29. }
  30.  

위 예제는 classpath의 루트로 잡은 디렉토리를 기준의 package형태로 설정되 파일을 로딩하여 사용할수 있도록 한다. 


이 이후의 코딩에는 class가 newInstance를 취한 후 method를 invoking해야 하는 과정을 거치게 되는데

한 가지 주의할 점은 해당 클래스를 반드시 reflection API를 이용하여 호출해야 한다는 점이다. 


대략 아래의 코드정도를 이용하여 main 메소드등을 호출하는 클래스를 작성할 수 있을 것이다. 

  1. public void invokeClass(String name, String[] args)
  2.     throws ClassNotFoundException,
  3.           NoSuchMethodException,
  4. {
  5.     Class c = loadClass(name);
  6.     Method m = c.getMethod("main"new Class[] { args.getClass() });
  7.     m.setAccessible(true);
  8.     int mods = m.getModifiers();
  9.     if (m.getReturnType() != void.class || !Modifier.isStatic(mods) ||
  10.         !Modifier.isPublic(mods)) {
  11.         throw new NoSuchMethodException("main");
  12.     }
  13.     try {
  14.         m.invoke(nullnew Object[] { args });
  15.     } catch (IllegalAccessException e) {
  16.         // This should not happen, as we have disabled access checks
  17.     }
  18. }


[출처]http://www.yunsobi.com/blog/136

Comment +0