2010년 4월 16일 금요일

WebService와 Namespace

오늘은 namespace와 관련된 글을 써 볼까 한다.
우선 이야기를 시작하기 전에 아래 두개의 Soap 요청에 대해서 살펴보기 바란다.

요청 메시지 샘플 1

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header/>
<soapenv:Body>
<ws:getUser xmlns:ws="http://ws.demo.amf.archnal.com/">
<userNo>1</userNo>
</ws:getUser>
</soapenv:Body>
</soapenv:Envelope>



요청메시지 샘플 2

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" >
<soapenv:Header/>
<soapenv:Body>
<getUser xmlns="http://ws.demo.amf.archnal.com/">
<userNo>1</userNo>
</getUser>
</soapenv:Body>
</soapenv:Envelope>


두 개의 차이점은 getUser 와 userNo의 관계이다.
첫 번째 요청은 userNo의 네임스페이스가 없다. getUser가 ws prefix를 가지고 있기 때문이다.
두 번째 요청은 userNo의 네임스페이스가 'http://ws.demo.amf.archnal.com/'으로 설정되어 있다.
따라서 두 개가 다른 요청이기 때문에 둘 중 하나는 에러가 발생한다.

나는 엘리먼트에 네임스페이스를 지정할 때 prefix를 생략하는 버릇이 있다. localPart와 namespaceURI 만 지정되면 유일한 노드가 되기 때문에 굳이 prefix를 사용하지 않아도 된다는 생각때문이다. 그런 작은 고정관념이 때때로 이런 치명적인 버그를 만들어 낸다.
userNo가 getUser와 같은 네임스페이스를 사용하거나 적어도 다른 네임스페이스라도 사용하고 있다면 에러가 발생하지 않았을 것이다. 하지만 아무런 네임스페이스도 사용하고 있지 않기 때문에 getUser에 prefix를 사용하지 않아서 문제가 발생하게 된 것이다. 이럴 경우에는 반드시 getUser의 prefix를 사용해 주어야 한다. prefix는 굳이 tns가 아니더라도 임의의 문자열로 지정하면 된다.

그럼 웹서비스는 왜 가장 하위 노드에 대해서는 네임스페이스를 만들지 않을까?
사실 웹서비스가 만들지 않은 게 아니라 안 만들도록 스키마를 정의했기 때문이다.
아래의 xs:schema의 속성중에 elementFormDefault="unqualified" 라고 지정된 부분을 qualified로 변경해 주면 된다. CXF에서도 이와 관련하여 설정하는 부분이 있을 것으로 생각되지만, 아직은 설정 방법을 찾아 보지 않았다. 하지만 표준 API의 디폴트 값은 특별한 경우가 아니라면 함께 준수해 주는 편이 좋다. 디폴트 값을 결정한 사람들도 많은 고심 끝에 의미를 부여했기 때문에 그 노력을 믿고 함께 따라 주는 것도 개발자의 미덕이 아닐까 생각한다.
그리고 추가적으로 한 가지 더 살펴보자 네임스페이스와 관련된 클래스가 javax.xml.namespace.QName이다. QName은 namespaceURI, localPart, prefix를 설정할 수 있도록 되어 있다. 하지만 상위 노드의 네임스페이스에 prefix를 지정하지 않을 경우에 네임스페이스를 갖지 않는 하위 노드를 만들어 낼 수 없다.
QName은 namespaceURI와 prefix를 지정하지 않게 되면 상위 노드의 네임스페이스 정보를 그대로 사용하기 때문이다. 특별한 경우가 아니라면 네임스페이스를 지정할 때 조그만 더 상상력을 발휘하여 prefix를 지정하는 습관을 갖도록 하자.


<wsdl:types>
<xs:schema
elementFormDefault="unqualified"
targetNamespace="http://ws.demo.amf.archnal.com/"
version="1.0"
xmlns:tns="http://ws.demo.amf.archnal.com/" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="getUser" type="tns:getUser" />

<xs:element minOccurs="0" name="user" type="tns:sampleUser" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="sampleUser">
<xs:sequence>
<xs:element minOccurs="0" name="email" type="xs:string" />
<xs:element minOccurs="0" name="loginId" type="xs:string" />
<xs:element minOccurs="0" name="loginPassword" type="xs:string" />
<xs:element minOccurs="0" name="name" type="xs:string" />
<xs:element name="userNo" type="xs:long" />
</xs:sequence>
</xs:complexType>

<xs:complexType name="getUser">
<xs:sequence>
<xs:element name="userNo" type="xs:long" />
</xs:sequence>
</xs:complexType>
</xs:schema>
</wsdl:types>

2010년 4월 15일 목요일

JAX-WS client 작성


  • Dispatch를 이용한 클라이언트 작성

  • jaxws:client를 이용한 클라이언트 작성

  • JaxWsProxyFactoryBean를 이용한 클라이언트 작성


CXF를 이용하여 웹서비스를 개발할 때 JAX-WS 클라이언트 만드는 방법을 이야기하고자 한다.
CXF가 Spring기반의 프레임웍이기 때문에 빈을 등록하여 사용하는 방법과 SOAP 메시지를 만들어서 사용하는 Java 표준 API(?) 방식을 비교해 보기 바란다.

이해를 돕기 위해 요청메시지와 응답메시지를 먼저 살펴보도록 한다.

요청 메시지
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ws="http://ws.demo.amf.archnal.com/">
   <soapenv:Header/>
   <soapenv:Body>
      <ws:getUser>
         <userNo>1</userNo>
      </ws:getUser>
   </soapenv:Body>
</soapenv:Envelope>


응답 메시지
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
   <soap:Body>
      <ns2:getUserResponse xmlns:ns2="http://ws.demo.amf.archnal.com/">
         <return>
            <email>email1@company.com</email>
            <loginId>loginid1</loginId>
            <loginPassword>passwd1</loginPassword>
            <name>name1</name>
            <userNo>1</userNo>
         </return>
      </ns2:getUserResponse>
   </soap:Body>
</soap:Envelope>


JAVA API를 이용하여 클라이언트 작성


아래는 WSDL 정보가 있다면 개발자가 아래와 같이 클라이언트를 작성할 수 있다.

표준 JAVA API 클라이언트
  public void testInvoke() throws Exception {
    String endpointAddress = "http://localhost:8080/demo/amfesb/sampleUserWs";
    
    QName serviceName = new QName("http://ws.demo.amf.archnal.com/", "SampleUserWsImplService");
    QName portName = new QName("http://ws.demo.amf.archnal.com/", "SampleUserWsImplPort");
    
    Service service = Service.create(serviceName);
    
    service.addPort(portName, SOAPBinding.SOAP11HTTP_BINDING, endpointAddress);
    
    Dispatch dispatch = service.createDispatch(portName, SOAPMessage.class, Service.Mode.MESSAGE);
    BindingProvider bp = (BindingProvider) dispatch;
    MessageFactory factory = ((SOAPBinding) bp.getBinding()).getMessageFactory();
    
    SOAPMessage request = factory.createMessage();
    SOAPBody body = request.getSOAPBody();
    QName payloadName = new QName("http://ws.demo.amf.archnal.com/", "getUser", "ws");
    SOAPBodyElement payload = body.addBodyElement(payloadName);
    
    
    QName userNoName = new QName("userNo");
    SOAPElement userNo = payload.addChildElement(userNoName);
    userNo.addTextNode("1");

    SOAPMessage reply = null;
    
    try {
      reply = dispatch.invoke(request);
    } catch(WebServiceException ex) {
      ex.printStackTrace();
    }
    
    body = reply.getSOAPBody();
    QName responseName = new QName("http://ws.demo.amf.archnal.com/", "getUserResponse");
    SOAPElement bodyElement = (SOAPElement) body.getChildElements(responseName).next();
    String responseMessageText = bodyElement.getTextContent();
    
  }


CXF 클라이언트 작성


CXF로 jaxws 클라이언트를 작성하기 위해서 SEI(Service Endpoint Interface)가 필요하다. SEI 클래스를 가지고 있지 않다고 하더라고 문제될 건 없다. http://credemol.blogspot.com/2010/04/cxf-codegen-plugin-maven.html 글에서 설명했던 것처럼 WSDL만 있으면 관련된 소스 코드를 자동으로 생성해 낼 수 있기 때문이다.

spring 빈 설정
<?xml version="1.0" encoding="UTF-8"?>
<beans
     xmlns="http://www.springframework.org/schema/beans"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns:jaxws="http://cxf.apache.org/jaxws"
     xmlns:cxf="http://camel.apache.org/schema/cxf"
    xsi:schemaLocation="
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://cxf.apache.org/jaxws 
    http://cxf.apache.org/schemas/jaxws.xsd
    http://camel.apache.org/schema/cxf 
    http://camel.apache.org/schema/cxf/camel-cxf.xsd
    ">
    <import resource="classpath:META-INF/cxf/cxf.xml" />
    <import resource="classpath:META-INF/cxf/cxf-servlet.xml" />
    <import resource="classpath:META-INF/cxf/cxf-extension-jaxrs-binding.xml" />
    <import resource="classpath:META-INF/cxf/cxf-extension-jaxws.xml" />
    <import resource="classpath:META-INF/cxf/cxf-extension-http.xml" />
    <import resource="classpath:META-INF/cxf/cxf-extension-addr.xml" />
    <import resource="classpath:META-INF/cxf/cxf-extension-soap.xml" />

  <jaxws:client id="sampleUserWsBiz" 
    serviceClass="com.archnal.amf.demo.ws.SampleUserWs"
    address="http://localhost:8080/demo/amfesb/sampleUserWs">
  </jaxws:client>
  
 
</beans>


위와 같이 spring 빈을 설정하였다면 client 소스코드는 아래와 같이 작성하면 된다.
getBean()은 스프링 빈 객체를 리턴하는 메서드다.

CXF 클라이언트
  public void testSampleUserWsBiz() {
    SampleUserWs ws = (SampleUserWs) getBean("sampleUserWsBiz");
    assertNotNull(ws);
    
    SampleUser user = ws.getUser(1L);
    assertNotNull(user);
  }


JaxWsProxyFactoryBean를 이용한 클라이언트 작성


jaxws:client 설정을 이용하여 클라이언트를 작성하면 여러가지로 편리한 점이 있지만, endpoint uri를 설정에서만 지정하기 때문에 uri를 동적으로 지정하고자 할 때 문제가 발생한다.
하지만 JaxWsProxyFactoryBean는 address가 프로퍼티이기 때문에 스프링 설정에서 지정할 수 있고 또한 호출시에 동적으로 설정할 수도 있다.

아래의 소스코드를 살펴보자.
호출할 때마다 address(endpoint uri)를 지정하여 서비스를 호출하는 예제다.

cxf configuration
   <bean id="userProxyFactory" class="org.apache.cxf.jaxws.JaxWsProxyFactoryBean">
      <property name="serviceClass" value="com.sds.shp.webservice.server.IUserWebService"/>
      <!-- 
      <property name="address" value="http://localhost:9002/HelloWorld"/>
       -->
    </bean>


서비스 호출 코드
package com.sds.shp.webservice.client;

import javax.annotation.Resource;

import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
import org.apache.cxf.jaxws.endpoint.dynamic.JaxWsDynamicClientFactory;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.sds.shp.domain.AuthWebservice;
import com.sds.shp.webservice.message.PutAccountInfoRequest;
import com.sds.shp.webservice.message.PutAccountInfoResponse;
import com.sds.shp.webservice.server.IUserWebService;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath*:/spring/context-*.xml" })

public class UserWebServiceClient {
  @Resource(name="userProxyFactory")
  private JaxWsProxyFactoryBean userProxyFactory;
  
  @Test
  public void testJaxWsProxyFactoryBean() throws Exception {
    userProxyFactory.setAddress("http://70.7.105.245:8080/store/ws/userWebService");
    IUserWebService userWebService = (IUserWebService) userProxyFactory.create();

    
    System.out.println("userWebService: " + userWebService);
    
    AuthWebservice authToken = new AuthWebservice();
    authToken.setTokenId("token");
    authToken.setTokenPasswd("pw");
    
    PutAccountInfoRequest request = new PutAccountInfoRequest();
    PutAccountInfoResponse response = userWebService.putAccountInfo(authToken, request);
    
    System.out.println("code: " + response.getResultStatus().getErrorCode());
    
  }
}


위의 소스 코드는 JaxWsProxyFactoryBean을 스프링 환경 설정 파일로 등록 한 후 테스트 코드에서 등록된 리소스를 Injection 받아서 사용하는 예제다. 호출할 때마다 address를 설정해서 사용하기 때문에 동적으로 endpoint uri를 지정하도록 되어 있지만 JaxWsProxyFactoryBean 빈을 여러 쓰레드에서 함께 사용하기 때문에 쓰레드에 안전하지 않게 작동될 가능성이 높다.
스프링에 등록하지 않고 실행될 때마다 JaxWsProxyFactoryBean 빈을 생성하는 아래의 소스코드를 참조하기 바란다. 매 요청시마다 새로운 JaxWsProxyFactoryBean이 생성되기 때문에 멀티쓰레드 환경에서도 안전하게 실행될 수 있다.
  @Test
  public void testJaxWsProxyFactoryBean2() throws Exception {
    JaxWsProxyFactoryBean proxyFactoryBean = new JaxWsProxyFactoryBean();
    proxyFactoryBean.setServiceClass(IUserWebService.class);
    proxyFactoryBean.setAddress("http://70.7.105.245:8080/store/ws/userWebService");
    IUserWebService userWebService = (IUserWebService) proxyFactoryBean.create();

    
    System.out.println("userWebService: " + userWebService);
    
    AuthWebservice authToken = new AuthWebservice();
    authToken.setTokenId("token");
    authToken.setTokenPasswd("pw");
    
    PutAccountInfoRequest request = new PutAccountInfoRequest();
    PutAccountInfoResponse response = userWebService.putAccountInfo(authToken, request);
    
    System.out.println("code: " + response.getResultStatus().getErrorCode());
    
  }

2010년 4월 13일 화요일

AOP 핵심 용어 정리

AOP 프로그램을 하면서도 가끔 용어들이 익숙치 않아서 작아 질 때가 있다.

조인포인트(joinpoint)
조인포인트는 애플리케이션 실행 중의 특정한 지점을 의미한다. 전형적인 조인포인트의 예로는 메서드 호출, 메서드 실행 자체, 클래스 초기화, 객체 생성시점 등이 있다. 조인포인트는 AOP 핵심 개념이며 애플리케이션의 어떤 지점에 AOP를 사용하여 추가적인 로직을 삽입할 지 정의한다.

어드바이스(advice)
특정 조인포인트에 실행하는 코드를 어드바이스라고 한다. 조인포인트 이전에 실행하는 Before 어드바이스와 이후에 실행하는 After 어드바이스를 비롯한 여러 종류의 어드바이스가 있다.

포인트컷(pointcut)
하나의 포인트컷은 여러 조인포인트의 집합체로 언제 어드바이스를 실행할 지 정의할 때 사용한다. 포인트컷을 만들어서, 애플리케이션 구성 요소에 어드바이스를 어떻게 적용할 지 상세하게 제어할 수 있다. 앞서 언급했듯이 가장 일반적으로 사용하는 조인포인트는 메서드 호출이다. 가장 일반적인 포인트컷은 특정 클래스에 있는 모든 메서드 호출로 구성된다. 종종, 여러분은 어드바이스 실행 지점을 좀 더 다양하게 제어할 필요가 있을 때 복잡한 형태로 포인트컷을 구성할 수도 있다.

애스팩트(aspect)
애스팩트는 어드바이스와 포인트컷을 조합한 것이다. 즉 이 조합물은 애플리케이션이 가지고 있어야 할 로직과 그것을 실행해야 하는 지점을 정의한 것이라고 할 수 있다.

위빙(weaving)
이것은 애플리케이션 코드 해당 지점에 애스팩트를 실제로 주입하는 과정을 말한다. 당연히, 컴파일 시점 AOP 솔루션은 이 작업을 컴파일 시점에 하여, 빌드 과정 중에 별도의 과정을 거친다. 마찬가지로, 실행 시점 AOP 설로션은 위빙 과정이 실행 중에 동적으로 일어난다.

타겟(target)
자신의 실행 흐름이 어떠한 AOP 처리로 인해 수정되는 객체를 타겟 객체라고 한다. 타겟 객체를 어드바이스가 적용된 객체라고도 한다.

인트로덕션(introduction)
인트로덕션은 객체 구조를 수정하여 새로운 메서드나 필드를 그 안에 추가할 수 있는 처리를 뜻한다. 여러분은 인트로덕션을 사용해서 어떤 객체가 특정 인터페이스를 명시적으로 구현하지 않고도 구현한 것으로 만들수 있다.

출처: Pro Spring 2.5

2010년 4월 9일 금요일

클래스 소스에 Delegate메서드 생성하기-JDT

이클립스에 Delegate 메서드 생성이라는 재미 있는 기능이 있다.
디자인 패턴을 공부해본 사람이라면 Delegate 패턴이라는 말이 익숙할 것이다.
간단히 설명하면 다음과 같다.
자바 클래스는 다중 상속을 지원하지 않는다. 이것과 관련이 깊다.
예를 들어 보자.
SomeClass에 goodMethod가 있다. 이것을 AnotherClass에서도 사용하고자 하는 경우가 있다.
가장 간단한 해결책은 상속이다.

public class AnotherClass extends SomeClass {
}

이렇게 사용하면 간단하긴 하지만 디자인 패턴에서는 상속을 그렇게 권장하지는 않는다.
하나밖에 없는 자리(상속)를 이미 사용해 버리면 향후에 후회할 일이 생길 가능성이 많기 때문이다.
무엇 보다도 AnotherClass와 SomeClass가 의미상으로 서로 상속관계를 맺는게 어색하다.
SomeClass의 메소드를 편하게 사용하는 것이 이유라면 말이다.
그래서 디자인패턴에서는 이러한 문제의 해결책으로 Delegate 패턴을 제시한다.

public class AnotherClass {
private SomeClass someClass = new SomeClass();

public void goodMethod() {
someClass.goodMethod();
}
}


언뜻 봐도 프로그래머가 할 일이 몇배는 많아 지는 것 같은 느낌이 든다. goodMethod 하나만 이렇게 하지는 않을 것이다. 아마도 SomeClass의 대부분의 메서드를 위와 같이 delegete 메서드를 만드는 것이 선뜻 내키지는 않을 것이다.
그래서 Eclipse는 Source -> Generate Delegate Methods 라는 훌륭한 메뉴를 만들어 제공하고 있다.
Delegate 메서드를 생성하고자 하는 필드를 선택하기만 하면 위와 같이 자동으로 소스코드를 생성해 준다.

Eclipse Plugin 개발자는 이런 JDT의 기본 액션에 감사를 해야 한다. ^^

나의 요구 사항은 다음과 같다.
Proxy based API로 구현된 RESTful 클라이언트를 생성하는 Wizard를 만들어라. 단 baseAddress와 proxy 인터페이스 정보는 Wizard에서 입력받는다.


private com.archnal.amf.sample.amfws.rs.SampleUserRs proxy = null;

public RestfulClientImpl() {
this.proxy = org.apache.cxf.jaxrs.client.JAXRSClientFactory.create("http://localhost:8080/amfws/restful", com.archnal.amf.sample.amfws.rs.SampleUserRs.class);
}


즉 proxy에 정의된 메서드를 모두 delegate하는 소스를 생성해야 했다.

아래의 소스는 생성자와 프로퍼티를 추가하고 프로퍼티에 대한 delegate 메소드를 추가하는 예제 소스이다. 눈여겨 볼 부분은 StubUtility2.getDelegatableMethods()와 전체 DelegateEntry 중에서 proxy와 관련된 엔트리만 필터링하는 부분이다.

private void modifyBeanClass(IProgressMonitor monitor) {
try {
String ln = IAmfUIConstants.LINE_SEP;

IType beanType = ProjectHelper.getJavaType(javaProject, beanClassName);
String fieldContents = "private " + proxyInterface + " proxy = null;" + ln;
IField field = beanType.createField(fieldContents, null, true, monitor);
StringBuffer sb = new StringBuffer();

sb.append("public ").append(beanType.getElementName()).append("() {").append(ln);
sb.append("\tthis.proxy = ").append("org.apache.cxf.jaxrs.client.JAXRSClientFactory.create(\"");
sb.append(baseAddress).append("\", ").append(proxyInterface).append(".class);").append(ln);
sb.append("}").append(ln);
IMethod constructor = beanType.createMethod(sb.toString(), null, true, monitor);
ProjectHelper.findJavaElement(javaProject, beanClassName).save(monitor, true);

CompilationUnit astRoot = ProjectHelper.parse(beanType);

VariableDeclarationFragment fragment =
ASTNodeSearchUtil.getFieldDeclarationFragmentNode(field, astRoot);

CodeGenerationSettings settings =
JavaPreferencesSettings.getCodeGenerationSettings(javaProject);
settings.createComments = true;



final ITypeBinding binding = ASTNodes.getTypeBinding(astRoot, beanType);
if(binding != null) {
DelegateEntry[] entries = StubUtility2.getDelegatableMethods(binding);
List result = new ArrayList();
if(fragment != null) {
IVariableBinding varBinding = fragment.resolveBinding();
for(int i = 0; i < entries.length; i++) {
if(varBinding == entries[i].field) {
result.add(entries[i]);
}
}
}
DelegateEntry[] methodToDelegate = result.toArray(new DelegateEntry[result.size()]);
AddDelegateMethodsOperation operation =
new AddDelegateMethodsOperation(astRoot, methodToDelegate, null, settings, true, true);

operation.run(monitor);
}


} catch(Exception ex) {
AmfPlugin.log(ex);
}
}

클래스 생성 시 인터페이스 메서드 implements 하기 - JDT

이클립스에서 클래스를 생성할 때 클랫스 생성 다이얼로그에서 인터페이스를 지정하면
생성된 클래스 소스 안에 인터페이스에 정의된 메서드들이 override되어 있다.
메서드 내의 로직은 없지만 컴파일 에러가 발생하지 않을 정도의 소스 코드가 생성된다.

이러한 작업은 클래스가 생성 된 이후에도 가능하다.
특정 인터페이스를 implements 한 후에 프로젝트 탐색뷰에서 해당 클래스를 선택한 후 팝업메뉴를 띄우거나 현재 작업 중인 에디터에서 팝업메뉴를 띄운 후 Source -> Override/Implement Methods 를 선택하면 override 하거나 implement 할 메서드 목록을 선택할 수 있는 창이 열린다.
메서드를 선택한 후 OK버튼을 클릭하면 메서드가 override/implements 된다.

요구사항 : 특정 인터페이스를 상속하는 클래스를 생성한다. 생성된 클래스는 인터페이스 메서드를 implements 한다.

JDT UI에서 처리되는 이러한 작업을 프로그래밍적으로 구현해야 한다면 어떻게 해야할까?
먼저 JDT에서는 어떻게 구현되었는지 살펴보았다.

org.eclipse.jdt.ui.actions.OverrideMethodsAction 에서 org.eclipse.jdt.internal.ui.dialogs.OverrideMethodDialog 으로부터 선택된 결과(org.eclipse.jdt.core.dom.IMethodBinding 목록)를 가지고 org.eclipse.jdt.internal.corext.codemanipulation.AddUnimplementedMethodsOperation 을 호출하는 구조로 되어 있다.

나의 요구사항에서는 OverrideMethodDialog가 필요없다. implements해야 할 메서드들이 이미 결정되어 있기 때문이다. 그래서 implements할 메서드 목록을 IMethodBinding으로 뽑아 내기만 하면 작업이 거의 완성될 것으로 판단했다. 어차피 나머지 작업은 AddUnimplementedMethodsOperation에서 처리될 것이기 때문이다.
스텁 코드에 대한 처리를 할 일이 있다면 org.eclipse.jdt.internal.corext.codemanipulation.StubUtility2 클래스에 대해서 알아두는 것이 유익하다.
아래의 소스 코드는 StubUtility2에서 IMethodBinding을 뽑아내는 로직이 들어있다.
살펴보기 바란다.


IType javaType = ProjectHelper.findType(javaProject, serviceClass);
CompilationUnit unit = ProjectHelper.parse(javaType);
final ITypeBinding binding = ASTNodes.getTypeBinding(unit, javaType);
if(binding != null) {
final IPackageBinding pack = binding.getPackage();
// 클래스에서 override하거나 implements 해야 할 모든 메서드 목록에 대한 정보를 리턴한다.
final IMethodBinding[] methods =
StubUtility2.getOverridableMethods(unit.getAST(), binding, false);
List list = new ArrayList();
for(IMethodBinding methodBinding: methods) {
// 상위 인터페이스에 정의된 메서드 목록만 추려낸다.
if(proxyInterface.equals(methodBinding.getDeclaringClass().getQualifiedName())) {
list.add(methodBinding);
}
}

IMethodBinding[] methodToOverride = list.toArray(new IMethodBinding[list.size()]);
try {
final ITypeBinding typeBinding= ASTNodes.getTypeBinding(unit, javaType);

// 시작위치는 지정하지 않는다. 가장 마지막에 추가될 것으로 판단된다.
int insertPos = -1;
// 오퍼레이션을 실행한다.
AddUnimplementedMethodsOperation operation = (AddUnimplementedMethodsOperation) createRunnable(unit, typeBinding, methodToOverride, insertPos, true);
operation.run(monitor);
} finally {
}
}


................

public static IWorkspaceRunnable createRunnable(CompilationUnit astRoot, ITypeBinding type, IMethodBinding[] methodToOverride, int insertPos, boolean createComments) {
AddUnimplementedMethodsOperation operation= new AddUnimplementedMethodsOperation(astRoot, type, methodToOverride, insertPos, true, true, true);
operation.setCreateComments(createComments);
return operation;
}


AddUnimplementedMethodsOperation의 생성자의 파라미터는 마지막 3개의 boolean 파라미터는 각각
imports: if the import edits should be applied
apply: if the resulting edit should be applied
save: if the changed compilation unit should be saved

참고로, 에디터가 열린 상태로 소스 코드를 생성하는 것이 아니기 때문에 save는 반드시 true로 지정되어한다.

2010년 4월 5일 월요일

AST로 배열값을 설정하는 annotation 추가하기

javax.ws.rs.Produces annotation은 문자열 배열을 값으로 가진다.
@Produces({"application/xml", "application/json"})

배열값을 가지는 annotation들은 기본적으로 1개만 지정되더라고 또는 전혀 지정되지 않더라도 문제가 되지 않는다.

@Produces("application/xml") 처럼 문자열 1개만 설정하도록 자바 소스를 생성하는 예제는 http://credemol.blogspot.com/2010/03/jdt-ast-abstract-syntax-tree.html에서 예제 소스를 작성해 두었다.

이번 글은 배열 값을 처리하는 것을 중심으로 설명하도록 한다.

@Produces(new String[] { "application/xml", "application/json"})

위와 같이 annotation을 설정했다면 어떻게 될까?
예상하는 바와 같이 "The value for annotation attribute Produces.value must be an array initializer" 라는 메시지가 출력되는 컴파일에러가 발생한다.

AST 용어로 접근하자면 new String[]은 ArrayCreation 이고, {"application/xml", "application/json"} 은 ArrayInitializer가 된다.
위의 에러메시지에서도 확인할 수 있듯이 annotation의 값은 array initializer가 되어야 한다.

@Produces와 같이 name=value 포맷으로 속성을 지정하는 것이 아니라 단순히 값만 지정하는 annotation은 SingleMemberAnnotation이다.

아래의 소스코드를 살펴보면 쉽게 이해할 수 있을 것이다.

예제 소스

public static void addSingleMemberAnnotation(AST ast, TypeDeclaration type, String annotationType, String[] literalValues) {
SingleMemberAnnotation anno = ast.newSingleMemberAnnotation();
anno.setTypeName(createTypeName(ast, annotationType));

ArrayInitializer arrayInitializer = ast.newArrayInitializer();
for(String value : literalValues) {
StringLiteral literal = ast.newStringLiteral();
literal.setLiteralValue(value.trim());
arrayInitializer.expressions().add(literal);
}

anno.setValue(arrayInitializer);
List list = (List) type.getStructuralProperty(TypeDeclaration.MODIFIERS2_PROPERTY);
list.add(anno);

}

CXF에서 jaxws:endpoint 설정하기

Resources:
http://cxf.apache.org/docs/jax-ws-configuration.html

jaxws:endpoint CXF에서 웹서비스(jax-ws)를 설정하는 엘리먼트다.
jaxws:endpoint는 아래와 같은 속성을 설정할 수 있다.
id: spring id
implementor: 웹서비스 구현 클래스명을 입력한다. 만약 웹서비스 구현 클래스를 별도의 스프링 빈으로 설정했다면 스프링빈아이디앞에 '#'을 붙혀셔 지정한다.
address: 절대 웹 URL을 입력해도 되지만 상대 경로로 지장할 수 있다. 예를들면 서블릿 컨텍스트가 /cxfsample이고 web.xml 에 지정한 servlet url mapping이 /service/* 인 경우에 address를 /sampleWs로 지정하였다면 서비스 경로는 /cxfsample/service/sampleWs 이 된다.
endpointName: port의 name 속성을 설정한다.

보통 cxf를 이용하여 웹서비스를 개발하는 경우에 스프링에 빈을 등록하기 때문에 보통 인터페이스와 구현클래스를 모두 개발하게 된다.
하지만 일반적으로 웹서비스와 관련된 annotation은 인터페이스에 설정하여 개발하는데 이렇게 되면 약간의 문제가 발생한다.
jaxws:endpoint로 설정하는 경우는 Java first 웹서비스 개발을 하게 되어 WSDL을 자동 생성되도록 하는데 CXF는 WSDL을 생성하는데 인터페이스에 설정된 annotation을 이용하지 않고 구현클래스에 있는 annotation을 이용한다.
만약 구현클래스에서 @WebService annotation을 설정하지 않았다면 WSDL을 아래와 같이 생성될 것이다.

생성된 WSDL 샘플

<wsdl:definitions name="SampleUserWsImplService" targetNamespace="http://ws.demo.amf.archnal.com/">
<wsdl:import location="http://localhost:8080/demo/amfesb/sampleUserWs?wsdl=sampleUserWs.wsdl" namespace="http://www.archnal.com/amf/demo/ws">
</wsdl:import>
...


위의 WSDL은 웹서비스를 제공하는데는 아무런 문제가 없지만 사소한 몇가지 문제가 있다.
우선 wsdl:definition의 targetNamespace가 우아하지 않다. 일반적으로 지정하는 namespace URI 형식이 아니다. 구현클래스의 패키지명을 역순으로 쭉 연결해 놓은 모양새가 보기에 좋지 않다. 두번째 문제로는 스키마를 동일 WSDL에 표현하지 않고 import 시키고 있다는 것이다. 원인은 서로 namespace가 다르기 때문이다. 네임스페이스를 동일하게 맞춰주면 단일 WSDL로 서비스에 대한 표현이 가능하다.

구현 클래스의 클래스 레벨에 아래와 같은 annotation을 지정하면 wsdl:definitions의 targetNamespace가
http://www.archnal.com/amf/demo/ws로 변경될 것이다.

@WebService(name="SampleUserWs"
,serviceName="SampleUserWs"
,targetNamespace="http://www.archnal.com/amf/demo/ws"
)







설정파일 샘플

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jaxws="http://cxf.apache.org/jaxws"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd">
<import resource="classpath:META-INF/cxf/cxf.xml" />
<import resource="classpath:META-INF/cxf/cxf-extension-soap.xml" />
<import resource="classpath:META-INF/cxf/cxf-servlet.xml" />

<jaxws:endpoint
id="sampleUserWs"
implementor="#sampleUserWsBean"
address="/sampleUserWs" >

</jaxws:endpoint>
<bean
id="sampleUserWsBean"
class="com.archnal.amf.amfws.ws.SampleUserWsImpl">
<property name="sampleUserService" ref="sampleUserService">
</property>
</bean>
</beans>

2010년 4월 4일 일요일

restful 서비스 클라이언트

JAX-RS는 RESTful 클라이언트에 대한 표준적인 방식을 제공하지 않는다.
CXF 프레임워크는 클라이언트 생성을 위한 두 가지 접근방식을 제공한다.
두 방식 모두 Spring을 이용하여 설정할 수 있다.

Proxy-based API


Proxy-based API는 RESTful서비스를 구현한 인터페이스가 있는 경우에 사용할 수 있다.
Proxy는 HTTP 요청을 생성하는 데 필요한 정보를 서비스 인터페이스를 이용한다.
org.apache.cxf.jaxrs.client.JAXRSClientFactory 클래스를 사용하며, proxy를 생성하기 위해 RESTful서비스 인터페이스를 입력값으로 전달해야 한다.
proxy가 생성되고 나면 RESTful서비스의 모든 메서드가 proxy에도 동일하게 존재하며, proxy의 메서드를 호출하면 RESTful 서비스에 메서드가 원격으로 호출된다.

HTTP centric clients


HTTP centric clients는 RESTful 서비스를 호출하기 위해 org.apache.cxf.jaxrs.client.WebClient 인스턴스를 이용한다. HTTP centric clients는 RESTful서비스 인터페이스를 알지 못해도 RESTful 서비스를 호출할 수 있지만 메서드의 I/O로 전달되는 도메인객체(model, VO)에 대한 정보는 알고 있어야 한다. 도메인 객체를 알지 못해도 처리는 가능하지만 XML을 파싱해야 하는 경우가 발생한다.

아래의 소스 파일에 WebClient를 이용하는 GET, POST method 예제와 JAXRSClientFactory를 이용하는 예제가 포함되어 있다.
눈여겨 볼 부분은 Proxy-based API의 서비스 인터페이스는 context path와 web.xml에서 설정되는 servlet url mapping 정보를 알 수 없기 때문에 예제에서와 같이 host, port 뿐만 아니라 추가 경로(/amfws/restful)에 대한 정보도 설정해 주어야 한다.
또한 WebClient는 매 요청마다 새롭게 만들어져야 한다. 그렇지 않으면 에러가 발생한다.

또 한가지 주의할 점은 RESTful 서비스의 리턴타입이 XML이 아닌 경우(예, String, boolean, int 등) JAXRSClientFactory의 proxy를 이용하는 경우에 RESTful 서비스의 메서드의 Produces에 반드시 text/plain으로 설정되어 있어야 한다. Produces가 설정되지 않은 경우에는 HTTP Status 406 에러가 발생한다.

RESTful client 예제
package com.archnal.amf.amfws.rs.client;

import java.io.IOException;
import java.io.InputStream;

import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.apache.cxf.jaxrs.client.JAXRSClientFactory;
import org.apache.cxf.jaxrs.client.WebClient;

import com.archnal.amf.amfws.biz.SampleUserBizService;
import com.archnal.amf.amfws.biz.SampleUserBizServiceImpl;
import com.archnal.amf.amfws.model.SampleUser;
import com.archnal.amf.amfws.rs.SampleUserRs;
import com.archnal.amf.amfws.rs.SampleUserRsImpl;

public class SampleUserRsClient {

public static void main(String[] args) throws Exception {
testProxyApi();

testWebClient();
}

private static void testProxyApi() throws Exception {
long userNo = 1L;
SampleUserRs proxy = null;
proxy = JAXRSClientFactory.create("http://localhost:8080/amfws/restful", SampleUserRs.class);
SampleUser user1 = proxy.getUser(userNo);
System.out.println("user1: " + user1.getName());

SampleUser newUser = createNewSampleUser();
proxy.register(newUser);
}

private static SampleUser createNewSampleUser() {
SampleUser newUser = new SampleUser();
newUser.setUserNo(System.currentTimeMillis());
newUser.setLoginId(System.currentTimeMillis() + "");
newUser.setName("new user");

return newUser;
}

private static void testWebClient() throws Exception {
WebClient webclient = WebClient.create("http://localhost:8080");
long userNo = 1L;
String path = null;

path = "/amfws/restful/sample/user/getUser/" + userNo;


//    SampleUser sampleUser = webclient.path(path).accept("application/xml").get(SampleUser.class);
SampleUser sampleUser = webclient.path(path).accept(MediaType.APPLICATION_XML_TYPE).get(SampleUser.class);
System.out.println(sampleUser.getName());

webclient = WebClient.create("http://localhost:8080");

path = "/amfws/restful/sample/user/register";
SampleUser newUser = createNewSampleUser();
Response response = webclient.path(path).accept("application/xml").post(newUser);
Object entity = response.getEntity();
System.out.println("entity: " + entity);
InputStream in = null;
try {
in = (InputStream) entity;
byte[] buf = new byte[in.available()];
in.read(buf);
System.out.println("content: '" + new String(buf) + "'");
} catch(IOException ex) {
ex.printStackTrace();
} finally {
if(in != null) {
in.close();
}
}

webclient = WebClient.create("http://localhost:8080");
//String value = webclient.path(path).accept("application/xml").post(newUser, String.class);
boolean value = webclient.path(path).post(newUser, Boolean.class);
System.out.println("value: " + value);
}
}



RESTful 인터페이스 예제
package com.archnal.amf.amfws.rs;

import java.util.List;

import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;

import com.archnal.amf.amfws.model.SampleUser;
@javax.ws.rs.Path("/sample/user")
@javax.ws.rs.Produces({"application/json", "application/xml", "text/plain"})

public interface SampleUserRs {
@POST
@Path("/register")
@Consumes({"application/xml"})
@Produces("text/plain")
boolean register(SampleUser user);

@javax.ws.rs.GET
@javax.ws.rs.Path("/getUser/{userNo}")
@Produces({"application/xml"})
SampleUser getUser(@PathParam("userNo") long userNo);


javax.ws.rs.core.Response login(String loginId, String loginPassword);
List searchByName(String userName);


SampleUser getUser(String loginId);
}

restful 서비스와 http method

HTTP 메소드라고 하면 GET과 POST가 먼저 떠오른다.
HTTP 메소드가 어떻게 설계되었든간에 GET과 POST만 있으면 흔히 말하는 CRUD를 모두 처리할 수 있다.
form의 메서드로 GET으로 설정한다고 해도 문제될 게 없지만
일반적으로 GET메서드는 조회나 삭제를 할 경우와 같이 파라미터가 간단한 경우에 사용되고,
POST는 insert나 update처럼 다수의 파라미터를 사용할 경우에 사용한다.
하지만 restful 관련 문서를 읽다보면 일반적으로 많이 사용하지 않은 PUT가 DELETE를 사용하는 경우를 볼 수 있다.
아마도 HTTP 메서드 자체가 각각의 용도에 맞게 설계된 듯 하다.

GET: 조회
POST: INSERT용
DELETE: 삭제
PUT: UPDATE

우리나라에서 DELETE나 PUT을 사용하는 것을 고운 시선으로 볼 것으로 생각하지 않지만 장점이 있다면 적극 사용하고 싶다.
서블릿의 경우 doGet, doPost등이 나누어져 있지만 흔히 doGet() { doPost(req, res); } 이런식으로 사용한다. 서블릿을 단위 기능별로 구현하는 습관때문에 그런 것으로 생각된다. 하지만 하나의 서블릿에서 doGet, doPost, doDelete, doPut을 구현할 수 있다면 서비스 단위로 서블릿을 구현할 수 있다.
앞으로 Http 메서드를 접하면서 작업을 할 때에는 그동안 해왔던 습관을 버리고 접근해야 겠다.

cxf와 apache-camel 연동하기

ESB 작업을 진행하고 있다. frontend channel은 cxf를 이용하여 웹서비스로 구현하고 있다.
cxf로 전달된 웹서비스를 enterprise intergration 엔진인 apache-camel과 연동하여 처리하고자 한다.
계속해서 자료를 찾아보고 있지만 일단 아래에 설명하는 것과 같이 작업하면 cxf와 camel이 연동하는 것을 확인했다.

spring 설정파일 작성



<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:cxf="http://camel.apache.org/schema/cxf"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://camel.apache.org/schema/cxf http://camel.apache.org/schema/cxf/camel-cxf.xsd
http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd
">


<import resource="classpath:META-INF/cxf/cxf.xml"/>
<import resource="classpath:META-INF/cxf/cxf-extension-soap.xml" />
<import resource="classpath:META-INF/cxf/cxf-servlet.xml" />

<bean id = "testBean" class="org.apache.camel.example.cxf.provider.TesterBean" />


<cxf:cxfEndpoint id="objectEndpoint"
serviceClass="org.apache.camel.example.cxf.amf.GreeterImpl"
address="/ObjectService"
endpointName="s:SoapOverHttpRouter"
serviceName="s:SOAPService"
wsdlURL="WEB-INF/wsdl/hello_world.wsdl"
xmlns:s="http://apache.org/hello_world_soap_http"/>
<camelContext id="test_context" xmlns="http://camel.apache.org/schema/spring">
<route>
<from uri="cxf:bean:objectEndpoint"/>
<to uri="bean:testBean?method=processObject"/>
</route>
</camelContext>


<cxf:cxfEndpoint id="orderEndpoint"
serviceClass="demo.order.OrderProcessImpl"
address="/OrderService"
endpointName="s:OrderProcessImplPort"
serviceName="s:OrderProcessImplService"
wsdlURL="classpath:wsdl/demo/order.wsdl"
xmlns:s="http://order.demo/"/>
<camelContext id="order_test_context" xmlns="http://camel.apache.org/schema/spring">
<route>
<from uri="cxf:bean:orderEndpoint"/>
<to uri="bean:testBean?method=processOrder"/>
</route>
</camelContext>


</beans>



TesterBean 클래스는 웹서비스 요청이 route되는 클래스다. 위의 설정 파일을 예로 든다면 ObjectService로 요청이 들어오면 testBean의 processObject 메소드로 전달된다. 메서드의 파라미터로는 Exchange 객체가 전달되는데, Exchange객체는 in(request), out(response), fault(exception)을 처리할 수 있도록 설계되었다고 생각하면 된다.
address는 서비스의 URL과 관련이 있으며, serviceName은 wsdl의 service name(service엘리먼트의 name 속성)과 동일하며 endpointName은 wsdl의 port name(port 엘리먼트의 name속성)과 동일하다.
wsdl파일은 위와 같이 WEB-INF 하위에서 지정할 수 도 있으며 classpath에서도 지정할 수 있다.
serviceClass는 웹서비스가 구현되어 있는 클래스명을 지정해야 한다. wsdl만으로 웹서비스를 제공해야 할 경우에는 cxf-codegen-plugin을 사용하여 서비스 클래스를 생성해야 한다.
서비스 클래스가 @WebService인 경우에는 exchange.getIn()의 바디가 자바 객체가 리턴되고 @WebServiceProvider로 제공되는 경우에는 SOAPMessage가 리턴된다.
라우팅될때 method명을 지정하면 클래스의 특정 메서드로 라우팅이 된다.
라우팅 메서드가 리턴 값이 있는 경우에는 파라미터로 전달되는 Exchange의 out에 값을 설정하더라도 리턴되는 값이 응답으로 전달되고, 리턴타입이 void인 경우에는 Exchange의 out에 설정한 값이 응답으로 전달된다.

TesterBean




package org.apache.camel.example.cxf.provider;

import java.util.List;

import javax.xml.namespace.QName;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPBodyElement;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPPart;

import org.apache.camel.Exchange;
import org.apache.cxf.message.MessageContentsList;


public class TesterBean {

public SOAPMessage processSOAP(Exchange exchange) {
System.out.println(getClass().getName() + " - processSOAP()");

SOAPMessage soapMessage = (SOAPMessage)exchange.getIn().getBody(List.class).get(0);
if (soapMessage == null) {
System.out.println("Incoming null message detected...");
return createDefaultSoapMessage("Greetings from Apache Camel!!!!", "null");
}

try {
SOAPPart sp = soapMessage.getSOAPPart();
SOAPEnvelope se = sp.getEnvelope();
SOAPBody sb = se.getBody();
String requestText = sb.getFirstChild().getTextContent();
System.out.println(requestText);
return createDefaultSoapMessage("Greetings from Apache Camel!!!!", requestText);
} catch (Exception e) {
e.printStackTrace();
return createDefaultSoapMessage("Greetings from Apache Camel!!!!", e.getClass().getName());
}
}

public String processObject(Exchange exchange) {
System.out.println("body: " + exchange.getIn().getBody(List.class).get(0).getClass().getName());
System.out.println("body value: " + exchange.getIn().getBody(List.class).get(0));
return "dummy";
}

public String processOrder(Exchange exchange) {
System.out.println("body: " + exchange.getIn().getBody(List.class).get(0).getClass().getName());
System.out.println("body value: " + exchange.getIn().getBody(List.class).get(0));
// if return type is void
//exchange.getOut().setBody("zzz");
return "dummy";

}

public static SOAPMessage createDefaultSoapMessage(String responseMessage, String requestMessage) {
System.out.println(TesterBean.class.getName() + " - createDefaultSoapMessage()");
try {
SOAPMessage soapMessage = MessageFactory.newInstance().createMessage();
SOAPBody body = soapMessage.getSOAPPart().getEnvelope().getBody();

QName payloadName = new QName("http://apache.org/hello_world_soap_http/types", "greetMeResponse",
"ns1");

SOAPBodyElement payload = body.addBodyElement(payloadName);

SOAPElement message = payload.addChildElement("responseType");

message.addTextNode(responseMessage + " Request was " + requestMessage);
return soapMessage;
} catch (SOAPException e) {
e.printStackTrace();
throw new RuntimeException(e);
}

}

}


restful 서비스 테스트 툴

테스트 프레임웍인 jmeter를 이용하여 jax-rs, jax-ws 관련 테스트를 할 수도 있지만 오늘은 브라우저에서 jax-rs 테스트 할 수 있는 방법을 이야기하고자 한다.

Firefox나 Chrome의 플러그인을 제공되는 Poster를 이용하면 restful 서비스를 쉽게 할 수 있다.
Chrome의 poster는 가볍지만 확인하는 정보도 적다.
Firefox가 설치되어 있다면 firefox용 poster를 설치해서 restful 서비스를 테스트 해 보기를 권장한다.

Firefox에 poster 설치하기


1. firefox 설치하기


http://www.mozilla.or.kr/ko/ 에서 firefox를 다운 받아 설치한다.

2. poster 설치하기


https://addons.mozilla.org/ko/firefox/addon/2691 에서 Firefox에 추가 버튼을 클릭한다.

firefox를 restart 시키면 하단의 상태표시줄에 "P" 라는 아이콘이 있다. 클릭하면 poster 창이 뜬다. UI는 직관적이므로 테스트 해보기 바란다.

Chrome에 poster 설치하기


1. chrome설치하기


(이건 뭐 알아서 하세요)

2. poster 설치하기


https://chrome.google.com/extensions 에서 poster라고 검색한 후 설치한다.

chrome용 플러그인은 아주 간단하다.

2010년 4월 3일 토요일

cxf-codegen-plugin (maven)

cxf에는 일반적인 웹서비스 프레임웍이 제공하듯이 wsdl to java 툴을 제공한다.
이번에 살펴볼 내용은 maven plugin으로 구현된 cxf-codegen-plugin을 살펴볼 것이다.


Resources:
http://cxf.apache.org/docs/maven-cxf-codegen-plugin-wsdl-to-java.html

자바 소스코드 생성



<plugin>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-codegen-plugin</artifactId>
<version>${cxf-version}</version>
<executions>
<execution>
<id>generate-sources</id>
<phase>generate-sources</phase>
<configuration>
<sourceRoot>${basedir}/target/generated/src/main/java</sourceRoot>
<wsdlOptions>
<wsdlOption>
<wsdl>${basedir}/src/main/resources/wsdl/hello_world.wsdl</wsdl>
</wsdlOption>
<wsdlOption>
<wsdl>${basedir}/src/main/resources/wsdl/demo/order.wsdl</wsdl>
</wsdlOption>
</wsdlOptions>
</configuration>
<goals>
<goal>wsdl2java</goal>
</goals>
</execution>
</executions>
</plugin>


wsdlOption 엘리먼트안의 wsdl 엘리먼트에 wsdl 파일을 지정해주면 1개 이상의 wsdl 파일을 이용하여 sourceRoot 안에 지정된 위치에 타입 클래스들에 대한 java 파일을 생성하고 maven의 output 디렉터리인 target/classes에 생성된 소스 파일들을 컴파일한다.

여러 개의 wsdl 파일 처리하기


그런데 wsdl이 추가 될 때마다 wsdlOption/wsdl 을 지정하는 것이 여간 번거로운 일이 아니다.
아래와 같이 지정하면 여러개의 wsdl 파일을 가지고 java source 코드를 생성할 수 있다.


<plugin>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-codegen-plugin</artifactId>
<version>${cxf-version}</version>
<executions>
<execution>
<id>generate-sources</id>
<phase>generate-sources</phase>
<configuration>
<sourceRoot>${basedir}/target/generated/src/main/java</sourceRoot>
<wsdlRoot>${basedir}/src/main/resources/wsdl</wsdlRoot>
<includes>
<include>**/*.wsdl</include>
</includes>
</configuration>
<goals>
<goal>wsdl2java</goal>
</goals>
</execution>
</executions>
</plugin>

wsdlRoot를 지정한 후에 includes/include에 위와 같이 지정하면 wsdlRoot 폴더 안에 포함되어 있는 모든 wsdl파일을 대상으로 타입 클래스를 생성한다.

이클립스를 사용하여 타입 클래스를 참조해야 한다면 소스 폴더를 target/generated/src/main/java로 지정해 두고 사용하면 편리하다.

클라이언트 소스 코드 생성


wsld2java 명령을 실행할 때 사용할 수 있는 여러개의 옵션들이 있다. 옵션 목록은 http://cxf.apache.org/docs/wsdl-to-java.html 에서 확인할 수 있따.
이러한 옵션들을 cxf-codegen-plugin에서도 사용할 있다.
예를들면 -client라는 옵션을 사용하면 웹서비스 클라이언트 코드를 생성해준다(실제 로직은 작성되어야 한다). -client 옵션을 사용하고자 하면 extraargs/extraarg를 이용하면 된다. extraargs 엘리먼트는 각 wsdlOption의 하위 노드에 설정되어 특정 wsdl 파일에만 적용할 수도 있고 아래와 같이 defaultOption을 이용하면 모든 wsdl에 적용할 수도 있다.

<plugin>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-codegen-plugin</artifactId>
<version>${cxf-version}</version>
<executions>
<execution>
<id>generate-sources</id>
<phase>generate-sources</phase>
<configuration>
<sourceRoot>${basedir}/target/generated/src/main/java</sourceRoot>
<wsdlRoot>${basedir}/src/main/resources/wsdl</wsdlRoot>
<defaultOptions>
<extraargs>
<extraarg>-client</extraarg>
</extraargs>
</defaultOptions>
<includes>
<include>**/*.wsdl</include>
</includes>
</configuration>
<goals>
<goal>wsdl2java</goal>
</goals>
</execution>
</executions>
</plugin>