레이블이 CXF인 게시물을 표시합니다. 모든 게시물 표시
레이블이 CXF인 게시물을 표시합니다. 모든 게시물 표시

2012년 7월 18일 수요일

CXF에서 Authorization 헤더 설정하기


CXF에서 HTTP Authorization 헤더에 Basic 방식으로 username, password를 전달할 때 아래와 같이 소스코드를 지정한다.



 org.apache.cxf.jaxws.JaxWsProxyFactoryBean clientFactory = new org.apache.cxf.jaxws.JaxWsProxyFactoryBean(); 
        clientFactory.setAddress("http://localhost:8080/svc-url"); // 서버 주소로 변경해 주세요
        clientFactory.setUsername(USERNAME); // username
        clientFactory.setPassword(PASSWORD); // password



HTTP Authorization 헤더의 Basic 방식인 
"username:password"가 base64 encoding 값으로 전달되는 것으로 확인되었습니다.

인증 정보 설정 후 요청 시 HTTP 요청 헤더 확인해 보시면 Authorization 헤더가 설정되어 있습니다.
 Authorization=[Basic Y2poY29uc3VtZXIxdidkxdxOndlbGNvbWUx]

2011년 5월 18일 수요일

CXF stringListProvider

cxf config file

---
<bean id="stringListProvider" class="com.archnal.util.StringListBodyWriter"/>
---

Java 구현 클래스
---
package com.archnal.util;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyWriter;

@Produces( { MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public class StringListBodyWriter implements MessageBodyWriter<List<String>> {

  public long getSize(List<String> t, java.lang.Class<?> type,
      java.lang.reflect.Type genericType,
      java.lang.annotation.Annotation[] annotations,
      javax.ws.rs.core.MediaType mediaType) {

    Iterator<String> i = t.iterator();
    long size = 0;
    while (i.hasNext()) {
      size += i.next().length();
    }

    return size;
  };

  @Override
  public boolean isWriteable(Class<?> type, Type genericType,
      Annotation[] annotations, MediaType mediaType) {
    return type.equals(ArrayList.class)
        && (mediaType.equals(MediaType.TEXT_PLAIN_TYPE)
            || mediaType.equals(MediaType.TEXT_XML_TYPE) || mediaType
            .equals(MediaType.APPLICATION_JSON_TYPE));
  }

  @Override
  public void writeTo(List<String> t, Class<?> type, Type genericType,
      Annotation[] annotations, MediaType mediaType,
      MultivaluedMap<String, Object> httpHeaders,
      OutputStream entityStream) throws IOException,
      WebApplicationException {

    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(
        entityStream));
    String ts = null;
    for (String aT : t) {
      ts += aT;
    }
    bw.write(ts);
    bw.flush();

  }
}
---

2011년 2월 21일 월요일

CXF 서비스 호출 시 발생하는 예외 처리 - Spring AOP Annotation

Spring에서 Annotation기반 AOP를 이용하려면 스프링 설정을 아래와 같이 지정해야 한다.

<?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:aop="http://www.springframework.org/schema/aop"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">

  <aop:aspectj-autoproxy />
  
</beans>

아래의 소스 코드는 CXF의 서비스 호출 시 에러 처리에 관련된 Aspect 소스 코드다.
아래의 소스 코드에서 @Service나 @Component 스테레오 타입이 지정되도록 해야한다.

package com.archnal.sample.cxf;

import javax.ws.rs.core.Response;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Service;

import com.archnal.sample.exception.BaseException;
import com.archnal.sample.exception.StatusCode;

@Aspect()
@Service("restfulServiceAspect")
public class RestfulServiceAspect {

  @Around("execution(public * com.archnal.sample.jaxrs.*ResponseServiceImpl.*(..))")
//  @Pointcut("execution(public * com.archnal.sample.jaxrs.*RestfulServiceImpl.*(..))")
  public Object handleRestfulService(ProceedingJoinPoint joinpoint) {
    System.out.println("around: " + joinpoint.getSignature().getName());
    
    Response response = null;
    try {
      Object[] args = joinpoint.getArgs();
      Object entity = joinpoint.proceed(args);
      
      return entity;
//      response = Response.ok(entity).build();
//      response = (Response)joinpoint.proceed(args);
      
    } catch(BaseException ex) {
      System.out.println("baseException: " + ex);
      response = Response.serverError().entity(create(ex)).build();
    } catch(Throwable ex) {
      System.out.println("throwable: " + ex);
      response = Response.serverError().entity(create(ex)).build();
    }
    return response;
  }
  
  @AfterThrowing(pointcut="execution(public * com.archnal.sample.jaxrs.*ResponseServiceImpl.*(..))", 
      throwing="e")
  public Object handleException(JoinPoint joinPoint, Throwable e) {
    System.out.println("afterThrowing: " + joinPoint.getSignature().getName());
    System.out.println("e: " + e);
    return Response.serverError().entity(create(e));
  }
  
  @Before("execution(public * com.archnal.sample.jaxrs.*ResponseServiceImpl.*(..))")
  public void before(JoinPoint joinPoint) {
    System.out.println("before: " + joinPoint.getSignature().getName());
  }
  
  @After("execution(public * com.archnal.sample.jaxrs.*ResponseServiceImpl.*(..))") 
  public void after(JoinPoint joinPoint) {
    System.out.println("after: " + joinPoint.getSignature().getName());
  }
  
  @AfterReturning(pointcut="execution(public * com.archnal.sample.jaxrs.*ResponseServiceImpl.*(..))", returning="result") 
  public void afterReturning(JoinPoint joinPoint, Object result) {
    System.out.println("afterReturning: " + joinPoint.getSignature().getName());
    System.out.println("result: " + result);
  }
  
  private ResponseResult create(BaseException ex) {
    ResponseResult header = new ResponseResult();
    header.setSuccess(false);
    header.setStatusCode(ex.getStatusCode().getCode());
    header.setMessage(ex.getMessage());
    
    return header;
  }
  
  private ResponseResult create(Throwable th) {
    if(th.getClass().isAssignableFrom(BaseException.class)) {
      return create((BaseException) th);
    }
    
    ResponseResult header = new ResponseResult();
    header.setSuccess(false);
    header.setStatusCode(StatusCode.UNKNOWN.getCode());
    header.setMessage(th.getMessage());
    
    return header;
    
  }
  

}


위에 메서드를 호출하면 에러가 발생하는 경우에 @AfterThrowing 으로 처리가 가능할까 싶어서 테스트를 해 보았으나,
예외가 발생하는 경우에 @AfterThrowing이 지정된 메서드를 호출하지만 메서드가 호출 된 이후에 Aspect는 여전히 원래 발생한 Throwable을 잡아서 제거하지 않고 던져 버린다. 따라서 서비스 Invoker에서는 여전히 에러가 발생한 것으로 인식하여 장황한 stack trace를 화면에 출력한다.
위의 예제에서는 에러가 발생하는 경우 잡아채서 클라이언트에게 serverError(500) 의 상태코드로 전달하면서 에러상태 코드를 전달하려는 의도로 작성된 코드다.
이런 요구사항에 @AterThrowing은 에러를 여전히 던지기 때문에 적합하지 않다. 실제로 @AfterThrowing에서 리턴하는 Response는 아무런 역할을 하지 못한다. 위의 소스 코드에서 처럼 @Around 에서 원래 메서드를 호출하고 에러가 발생하는 경우에 Response를 생성하여 리턴하도록 해야한다.
하지만 이런 방식에서도 문제가 하나 있다. Restful서비스의 원래 메서드가 Response를 리턴하도록 되어 있어야 한다. 일반 Pojo 클래스를 리턴하는 경우에는 ClassCastException이 발생한다. 이처럼 에러를 일괄적으로 처리하기 위해서는 꼭 Response가 아니더라도 ResponseMessage 같은 일관된 메시지 형태의 객체를 리턴해야만 ClassCastException이 발생하지 않는다.

위의 Aspect가 적용된 상태에서 서비스를 호출하였을 때 정상적인 경우에 아래와 같이 화면에 출력된다.
before: get
around: get
after: get
afterReturning: get
result: org.apache.cxf.jaxrs.impl.ResponseImpl@170a5a3

에러가 발생하는 경우에는 아래와 같이 화면에 출력된다.

before: get
around: get
after: get
afterThrowing: get
e: com.archnal.sample.exception.BaseException: category: BIZ_LOGIC_ERROR, status: USER_NOT_FOUND, code: E_102
afterReturning: get
result: org.apache.cxf.jaxrs.impl.ResponseImpl@15f8a0a

뭔 얘기를 하는건지 나만 알 수 있는 말을 적은 거 같다. 아는 사람은 알겠지 뭐~~

2011년 2월 18일 금요일

CXF의 Custom Invoker 개발

CXF에서 요청을 받아서 서비스를 실행하고 응답을 리턴하는 프로세스를 살펴 보면 필요에 따라 각각을 커스터마이즈 할 수 있다.
요청 프로세스는 inbound chain 이라 하여 아래의 단계를 거친다.
RECEIVE
(PRE/USER/POST)_STREAM
READ
(PRE/USER/POST)_PROTOCOL
UNMARSHAL
(PRE/USER/POST)_LOGICAL
PRE_INVOKE
INVOKE
POST_INVOKE

응답 프로세스는 outbound chain 이라 하여 아래의 단계를 거친다.
SETUP
(PRE/USER/POST)_LOGICAL
PREPARE_SEND
PRE_STREAM
PRE_PROTOCOL
WRITE
MARSHAL
(USER/POST)_PROTOCOL
(USER/POST)_STREAM
SEND

각 단계의 구현을 InInterceptor나 OutInterceptor를 이용하여 구현할 수 있다.

하지만 이번 포스트에서는 Invoker에 대해서만 설명하도록 한다.

Invoker란 inbound chain의 INVOKE 단계에서 호출되는 것으로 실제 서비스를 호출하는 기능을 담당한다. CXF는 기본적인 Invoker를 제공하지만 서비스 호출 전,후에 특별한 처리를 해야 한다면 별도의 Invoker를 개발하여 사용할 수 있다.

Invoker 설정
cxf 설정 파일에서 아래와 같이 지정한다.

  <jaxrs:server id="restfulServer" address="/rest">
    <jaxrs:features>
      <cxf:logging/>
    </jaxrs:features>
    <jaxrs:invoker>
      <ref bean="baseInvoker"/>
    </jaxrs:invoker>
    
    <jaxrs:serviceBeans>
      <ref bean="userRestfulService"/>
    </jaxrs:serviceBeans>
  </jaxrs:server>

Customized Invoker 개발
아래의 소스는 JAXRS에서 기본적으로 사용하는 JAXRSInvoker를 상속받아서 구현한 것으로 에러 처리 기능이 추가된 Invoker라 할 수 있다.

JAX-RS 서비스에서 정상처리된 경우에는 서비스가 리턴한 객체를 리턴하지만 서비스 처리도중 에러가 발생하는 경우에는 에러 코드와 에러 메시지를 엔티티로 포함하고 있는 Response 객체를 리턴한다.

package com.archnal.sample.cxf;

import java.util.Locale;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.Response;

import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.jaxrs.JAXRSInvoker;
import org.apache.cxf.message.Exchange;
import org.apache.cxf.message.MessageContentsList;
import org.springframework.stereotype.Service;

import com.archnal.sample.exception.BaseException;
import com.archnal.sample.exception.StatusCode;
import com.archnal.sample.exception.StatusMessageBuilder;

@Service("baseInvoker")
public class BaseInvoker extends JAXRSInvoker {
  @Resource(name="statusMessageBuilder")
  private StatusMessageBuilder statusMessageBuilder;
  
  @Override
  public Object invoke(Exchange exchange, Object request) {
    Response response = null;

    HttpServletRequest servletRequest = 
      (HttpServletRequest)exchange.getInMessage().get("HTTP.REQUEST");

    Locale clientLocale = null;
    if(servletRequest != null) {
      clientLocale = servletRequest.getLocale();
    }
    try {
      Object object = super.invoke(exchange, request);
      return object;
    } catch(Fault fault) {
      Throwable cause = fault.getCause();
      if(cause.getClass().isAssignableFrom(BaseException.class)) {
        response = Response.serverError().entity(create((BaseException) cause, clientLocale)).build();
        
      } else {
        response = Response.serverError().entity(create(cause, clientLocale)).build();
      }
    }
    
    return new MessageContentsList(response);
  }

  private ResponseResult create(BaseException ex, Locale locale) {
    ResponseResult header = new ResponseResult();
    header.setSuccess(false);
    header.setStatusCode(ex.getStatusCode().getCode());
    header.setMessage(statusMessageBuilder.getErrorMessage(ex, locale));
    
    return header;
  }
  
  private ResponseResult create(Throwable th, Locale locale) {
    
    ResponseResult header = new ResponseResult();
    header.setSuccess(false);
    header.setStatusCode(StatusCode.UNKNOWN.getCode());
    header.setMessage(statusMessageBuilder.getErrorMessage(StatusCode.UNKNOWN, locale));
    
    return header;
    
  }
}

2011년 2월 17일 목요일

CXF의 @FormParam("") 사용 시 WADL 생성 에러

사용자 등록 서비스를 가정해 보자.
loginId, firstName, lastName, email, password를 FormParam으로 입력받아야 할 경우
5개의 파라미터를 갖는 메서드를 만들어야 한다.

public void addUser(
@FormParam("loginId") String loginId,
@FormParam("firstName") String firstName,
@FormParam("lastName") String lastName,
@FormParam("email") String email,
@FormParam("password" String password) throws Exception;

이럴 경우에 User라는 VO 클래스가 존재한다면 아래와 같이 인터페이스를 정의할 수 있다.
public void addUser(@FormParam("") User user) throws Exception;

각각 input 의 name과 User 클래스의 필드 명을 비교하여 User 객체의 필드 값을 설정해 주기 때문에 메서드도 깔끔하고 개발하기도 용이하다.

public class User {
private String loginId;
private String firstName;
private String lastName;
private String email;
private String password;

// getter / setter methods.
}

하지만 이렇게 리소스 메서드를 정의할 경우 CXF가 WADL 을 생성할 때 아래와 같은 에러가 발생한다.

심각: Servlet.service() for servlet CXFServlet threw exception
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2882)
at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:100)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:390)
at java.lang.StringBuilder.append(StringBuilder.java:119)
at org.apache.cxf.jaxrs.model.wadl.WadlGenerator.doWriteParam(WadlGenerator.java:465)
at org.apache.cxf.jaxrs.model.wadl.WadlGenerator.doWriteBeanParam(WadlGenerator.java:454)

이러한 문제점은 CXF 내부의 문제로 CXF 버전 2.3.0 에서부터 발생하지 않는다.
따로 해결책을 찾으려 애쓰지 말고 CXF 버전을 올려 보시길...

2010년 9월 28일 화요일

CXF에서 Log4J 사용하기

Resources:
http://www.techper.net/2008/01/30/configuring-cxf-logging-to-go-through-log4j/
http://gtko.springnote.com/pages/4826007

CXF에서 사용하는 로그는 별도로 지정하지 않으면 디폴트로 java.util.logging.Logger를 사용한다.
CXF에서 log4j를 사용하려면 다음과 같이 지정해 주면 된다.

1. -D옵션을 이용하여 시스템 property로 지정한다.
-Dorg.apache.cxf.Logger=org.apache.cxf.common.logging.Log4jLogger

2. META-INF/cxf/org.apache.cxf.Logger 파일로 설정한다.
-D옵션을 설정하기 용이치 않으면 위의 클래스패스에 위치한 파일에 아래와 같이 설정한다.
org.apache.cxf.common.logging.Log4jLogger

일반적으로 -D 옵션을 이용하는 방법과 동일한 결과를 프로그래밍적으로 해결하기 위한 방법으로
System.setProperty("org.apache.cxf.Logger", "org.apache.cxf.common.logging.Log4jLogger");
와 같이 설정하여 처리하고자 한다면 정상적으로 작동하지 않을 것이다.
이는 Spring이 초기화 되는 시점에 CXF 관련 빈들도 초기화 하는데 위의 코드가 반드시 CXF 빈들보다 먼저 초기화 된다고 볼 수 없기 때문이다.

1 또는 2의 방법중에서 하나만 설정해도 정상적으로 동작한다.
특히나 Inbound/Outbound 메시지를 보고 싶다면 log4j 설정파일에서
INFO레벨로 로그를 설정해주면 된다.

<logger name="org.apache.cxf" additivity="false">
<level value="INFO" />
<appender-ref ref="File_Appender" />
</logger>

2010년 7월 1일 목요일

CXF에서 WS-Security 사용하기.

Resources:

http://www.jroller.com/gmazza/entry/cxf_x509_profile

http://www.jroller.com/gmazza/entry/cxf_usernametoken_profile#UTCXF2

원문에서는 암호화 라이브러리인 bounce-castle을 설치하라고 설명하지만 예제에서 사용하는 암호화 앨거리듬(algorithm)을 사용하는 경우에는 굳이 설치가 필요하지 않다.

keystore에 key-pair 생성



keytool -genkey -alias myservicekey -keyalg RSA -sigalg SHA1withRSA -keypass skpass -storepass sspass -keystore serviceKeystore.jks -dname "cn=localhost"

keytool -genkey -alias myclientkey -keyalg RSA -sigalg SHA1withRSA -keypass ckpass -storepass cspass -keystore clientKeystore.jks -dname "cn=clientuser"


인증서 생성 후 keystore에 등록




keytool -export -rfc -keystore clientKeystore.jks -storepass cspass -alias myclientkey -file MyClient.cer

keytool -import -trustcacerts -keystore serviceKeystore.jks -storepass sspass -alias myclientkey -file MyClient.cer -noprompt


keytool -export -rfc -keystore serviceKeystore.jks -storepass sspass -alias myservicekey -file MyService.cer

keytool -import -trustcacerts -keystore clientKeystore.jks -storepass cspass -alias myservicekey -file MyService.cer -noprompt


인증서가 각각 서로의 종단 키스토어 파일에 등록되었기 때문에 인증서 파일은 삭제해도 상관없다. 키스토어 파일을 cxf에서 인식할 수 있도록 클래스 패스에 복사하도록 한다.
예를 들어 서비스 프로바이더 측에서는 serviceKeystore.jks 파일을 java/main/resources 에 복사한다.

서버측 웹서비스 개발 및 속성


스프링 설정

src/main/resources/spring/spring-config.xml


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

<!-- component scan -->
<context:component-scan base-package="sample.cxf" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
<context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
<context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
</beans>




src/main/resources/spring/cxf-config.xml

<?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://cxf.apache.org/core"
xmlns:jaxrs="http://cxf.apache.org/jaxrs" 
xmlns:jaxws="http://cxf.apache.org/jaxws"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd
http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd
">


<!-- Load CXF modules from cxf.jar -->
<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-extension-jaxrs-binding.xml" />
<import resource="classpath:META-INF/cxf/cxf-servlet.xml" />

<!-- Enable message logging using the CXF logging feature -->
<cxf:bus>
<cxf:features>
<cxf:logging />
</cxf:features>
</cxf:bus>

<bean id="myPasswordCallback"
class="sample.cxf.webservice.server.ServerPasswordCallback"/>

<jaxws:endpoint id="userWss4jWebService"
implementor="#UserWebService"
address="/wss4jUserWebService">
<jaxws:outInterceptors>
<ref bean="TimestampSignEncrypt_Response"/>
</jaxws:outInterceptors>
<jaxws:inInterceptors>
<ref bean="TimestampSignEncrypt_Request"/>
</jaxws:inInterceptors>
</jaxws:endpoint>


<jaxws:endpoint id="userImageWebService"
implementor="#UserImageWebService"
address="/userImageWebService">
<jaxws:properties>
<entry key="mtom-enabled" value="true"/>
</jaxws:properties>
<jaxws:outInterceptors>
<ref bean="TimestampSignEncrypt_Response"/>
</jaxws:outInterceptors>
<jaxws:inInterceptors>
<ref bean="TimestampSignEncrypt_Request"/>
</jaxws:inInterceptors>
</jaxws:endpoint>

<bean id="TimestampSignEncrypt_Request"
class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor">
<constructor-arg>
<map>
<entry key="action" value="Timestamp Signature Encrypt"/>
<entry key="signaturePropFile" value="serviceKeystore.properties"/>
<entry key="decryptionPropFile" value="serviceKeystore.properties"/>
<entry key="passwordCallbackClass" value="sample.cxf.webservice.server.ServiceKeystorePasswordCallback"/>

</map>
</constructor-arg>
</bean>

<bean id="TimestampSignEncrypt_Response"
class="org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor">
<constructor-arg>
<map>
<entry key="action" value="Timestamp Signature Encrypt"/>
<entry key="user" value="myservicekey"/>
<entry key="signaturePropFile" value="serviceKeystore.properties"/>
<entry key="encryptionPropFile" value="serviceKeystore.properties"/>
<entry key="encryptionUser" value="useReqSigCert"/>
<entry key="passwordCallbackClass" value="sample.cxf.webservice.server.ServiceKeystorePasswordCallback"/>
<entry key="signatureParts" value="{Element}{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd}Timestamp;{Element}{http://schemas.xmlsoap.org/soap/envelope/}Body"/>
<entry key="encryptionParts" value="{Element}{http://www.w3.org/2000/09/xmldsig#}Signature;{Content}{http://schemas.xmlsoap.org/soap/envelope/}Body"/>
<entry key="encryptionSymAlgorithm" value="http://www.w3.org/2001/04/xmlenc#tripledes-cbc"/>
</map>
</constructor-arg>
</bean>

<jaxws:endpoint id="userWebService" 
implementor="#UserWebService"
wsdlLocation="WEB-INF/wsdl/simpleUserWebService-usernametoken.wsdl"
address="/userWebService">

<jaxws:inInterceptors>
<bean class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor">
<constructor-arg>
<map>
<entry key="action" value="UsernameToken"/>
<entry key="passwordType" value="PasswordText"/>
<entry key="passwordCallbackRef">
<ref bean="myPasswordCallback"/>
</entry>
</map>
</constructor-arg>
</bean>
</jaxws:inInterceptors>
</jaxws:endpoint>

</beans>




src/main/resources/serviceKeystore.properties


org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin
org.apache.ws.security.crypto.merlin.keystore.type=jks
org.apache.ws.security.crypto.merlin.keystore.password=sspass
org.apache.ws.security.crypto.merlin.keystore.alias=myservicekey
org.apache.ws.security.crypto.merlin.file=serviceKeystore.jks



serviceKeystore.jsk 파일이 src/main/resources 폴더에 저장되어 있어야 한다.

sample.cxf.webservice.server.ServiceKeystorePasswordCallback

package sample.cxf.webservice.server;

import java.io.IOException;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;

import org.apache.ws.security.WSPasswordCallback;

public class ServerPasswordCallback implements CallbackHandler {

@Override
public void handle(Callback[] callbacks) throws IOException,
UnsupportedCallbackException {
WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];

if("joe".equals(pc.getIdentifier())) {
if(! pc.getPassword().equals("joespassword")) {
throw new IOException("Username/Password failure. - invalid password");
}
} else {
throw new IOException("Username/Password failure. - invalid username");
}
}
}



위 파일을 제외한 나머지 부분들은 일반 웹서비스 개발과 동일하다.
웹 컨텍스트 패스는 cxf-sample-project다. 클라이언트에서 서비스 정보를 설정할 때 컨텍스트 패스를 설정하므로 기억해 두어야 한다.

클라이언트 개발 및 설정


클라이언트 프로그램은 일반적으로 WSDL을 이용하여 CXF툴로 자바 소스를 생성한다. 일반적으로 생성되는 자바 소스의 패키지 명은 WSDL의 targetNamespace로 생성된다. 위의 경우에는 com.mycompany.sample.cxf.{subpackage}.. 로 생성된다.

src/main/resources/spring/spring-config.xml

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

<!-- component scan -->
<context:component-scan base-package="com.mycompany.sample.cxf" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
<context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
<context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
</beans>





src/main/resources/spring/cxf-config.xml

<?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://cxf.apache.org/core"
xmlns:jaxrs="http://cxf.apache.org/jaxrs" 
xmlns:jaxws="http://cxf.apache.org/jaxws"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd
http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd
">


<!-- Load CXF modules from cxf.jar -->
<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-extension-jaxrs-binding.xml" />
<import resource="classpath:META-INF/cxf/cxf-servlet.xml" />

<!-- Enable message logging using the CXF logging feature -->
<cxf:bus>
<cxf:features>
<cxf:logging />
</cxf:features>
</cxf:bus>

<bean id="userClient" class="com.mycompany.sample.cxf.webservice.user.UserWebService"
factory-bean="clientFactory" factory-method="create">
</bean>

<bean id="clientFactory" class="org.apache.cxf.jaxws.JaxWsProxyFactoryBean">
<property name="serviceClass" value="com.mycompany.sample.cxf.webservice.user.UserWebService"></property>
<property name="address" value="http://localhost:8080/cxf-sample-project/ws/wss4jUserWebService"/>
<property name="inInterceptors">
<list>
<ref bean="TimestampSignEncrypt_Response"/>
</list>
</property>
<property name="outInterceptors">
<list>
<ref bean="TimestampSignEncrypt_Request"/>
</list>
</property>
</bean>

<bean id="userImageClient" class="com.mycompany.sample.cxf.webservice.userimage.UserImageWebService"
factory-bean="userImageClientFactory" factory-method="create">
</bean>

<bean id="userImageClientFactory" class="org.apache.cxf.jaxws.JaxWsProxyFactoryBean">
<property name="serviceClass" value="com.mycompany.sample.cxf.webservice.userimage.UserImageWebService"></property>
<property name="address" value="http://localhost:8080/cxf-sample-project/ws/userImageWebService"/>
<property name="inInterceptors">
<list>
<ref bean="TimestampSignEncrypt_Response"/>
</list>
</property>
<property name="outInterceptors">
<list>
<ref bean="TimestampSignEncrypt_Request"/>
</list>
</property>
</bean>

<bean class="org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor" 
id="TimestampSignEncrypt_Request">
<constructor-arg>
<map>
<entry key="action" value="Timestamp Signature Encrypt"></entry>
<entry key="user" value="myclientkey"></entry>
<entry key="signaturePropFile" value="clientKeystore.properties"/>
<entry key="encryptionPropFile" value="clientKeystore.properties"></entry>
<entry key="encryptionUser" value="myservicekey"/>
<entry key="passwordCallbackClass" value="com.mycompany.sample.cxf.webservice.user.ClientKeystorePasswordCallback"/>
<entry key="signatureParts" value="{Element}{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd}Timestamp;{Element}{http://schemas.xmlsoap.org/soap/envelope/}Body"/>
<entry key="encryptionParts" value="{Element}{http://www.w3.org/2000/09/xmldsig#}Signature;{Content}{http://schemas.xmlsoap.org/soap/envelope/}Body"/>
<entry key="encryptionSymAlgorithm" value="http://www.w3.org/2001/04/xmlenc#tripledes-cbc"/>
</map>
</constructor-arg>
</bean>

<bean class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor"
id="TimestampSignEncrypt_Response">
<constructor-arg>
<map>
<entry key="action" value="Timestamp Signature Encrypt"/>
<entry key="signaturePropFile" value="clientKeystore.properties"/>
<entry key="decryptionPropFile" value="clientKeystore.properties"/>
<entry key="passwordCallbackClass" value="com.mycompany.sample.cxf.webservice.user.ClientKeystorePasswordCallback"/>
</map>
</constructor-arg>
</bean>
</beans>



위의 설정 파일에서 서비스 경로를 주의 깊게 봐야 한다. 서버에서 설정한 컨텍스트 경로와 일치하는지 살펴봐야 한다.
클라이언트 서비스는 두 개다. 일반 SOAP 기반 메시지를 사용하는 웹서비스와 MTOM을 이용한 바이너리 파일 전송 웹서비스다.
실제로 WS-Security에 관련된 사항을 위해 클라이언트 코드가 변경되지는 않는다. CXF에서는 WSS4JInInterceptor와 WSS4JOutInterceptor 에 대한 설정만으로 WS-Security가 가능하다.

java/main/resources/clientKeystore.properties

org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin
org.apache.ws.security.crypto.merlin.keystore.type=jks
org.apache.ws.security.crypto.merlin.keystore.password=cspass
org.apache.ws.security.crypto.merlin.keystore.alias=myclientkey
org.apache.ws.security.crypto.merlin.file=clientKeystore.jks




com.mycompany.sample.cxf.webservice.user.ClientKeystorePasswordCallback

package com.mycompany.sample.cxf.webservice.user;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;

import org.apache.ws.security.WSPasswordCallback;
import org.apache.ws.security.handler.WSHandlerConstants;

public class ClientKeystorePasswordCallback implements CallbackHandler {

private Map passwords = new HashMap();

public ClientKeystorePasswordCallback() {
super();

passwords.put("myclientkey", "ckpass");
}

@Override
public void handle(Callback[] callbacks) throws IOException,
UnsupportedCallbackException {

for(int i = 0; i < callbacks.length; i++) {
WSPasswordCallback pc = (WSPasswordCallback) callbacks[i];

String pass = passwords.get(pc.getIdentifier());

if(pass != null) {
pc.setPassword(pass);
return;
}
}
}
}
WS-Security 클라이언트 샘플 - 일반 웹서비스
package com.mycompany.sample.cxf.webservice.user;

import javax.annotation.Resource;

import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.interceptor.LoggingInInterceptor;
import org.apache.cxf.interceptor.LoggingOutInterceptor;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.mycompany.sample.cxf.webservice.message.GetUserRequest;
import com.mycompany.sample.cxf.webservice.message.GetUserResponse;

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

@Resource(name="userClient")
protected UserWebService userClient;
@Test
public void testWss4J() throws Exception {
System.out.println("userClient: " + userClient);

Client client = ClientProxy.getClient(userClient);

client.getInInterceptors().add(new LoggingInInterceptor());
client.getOutInterceptors().add(new LoggingOutInterceptor());

GetUserRequest request = new GetUserRequest();
GetUserResponse response = userClient.getUser(request);

String code = response.getMessageStatus().getCode();
String message = response.getMessageStatus().getMessage();

System.out.println("=======> code: " + code + ", message: " + message);
}
}
WS-Security 클라이언트 샘플 - MTOM 웹서비스
package com.mycompany.sample.cxf.webservice.user;

import java.io.File;

import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.annotation.Resource;

import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.interceptor.LoggingInInterceptor;
import org.apache.cxf.interceptor.LoggingOutInterceptor;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.mycompany.sample.cxf.webservice.message.GetUserRequest;
import com.mycompany.sample.cxf.webservice.message.GetUserResponse;
import com.mycompany.sample.cxf.webservice.message.MessageStatus;
import com.mycompany.sample.cxf.webservice.userimage.UploadUserImageRequest;
import com.mycompany.sample.cxf.webservice.userimage.UploadUserImageResponse;
import com.mycompany.sample.cxf.webservice.userimage.UserImageWebService;

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

@Resource(name="userImageClient")
protected UserImageWebService userImageClient;
@Test
public void testWss4J() throws Exception {
System.out.println("userImageClient: " + userImageClient);

Client client = ClientProxy.getClient(userImageClient);

client.getEndpoint().put("mtom-enabled", "true");

client.getInInterceptors().add(new LoggingInInterceptor());
client.getOutInterceptors().add(new LoggingOutInterceptor());


UploadUserImageRequest request = new UploadUserImageRequest();
request.setUploadUserId("kim");

DataSource dataSource = new FileDataSource(new File("d:/test3.zip"));
DataHandler image = new DataHandler(dataSource);
request.setUserImage(image);

UploadUserImageResponse response = userImageClient.uploadImage(request);

MessageStatus status = response.getStatus();

System.out.println("status: " + status);
if(status != null) {
String code = status.getCode();
String message = status.getMessage();

System.out.println("=======> code: " + code + ", message: " + message);

}
}
}
위의 소스 코드를 살펴보면 알겠지만 서버 소스와 클라이언트 소스에서는 WS-Security에 관련된 소스가 전혀 없다. CXF spring 설정 파일에서만 웹서비스 또는 웹서비스 클라이언트에 대한 설정만 하면 된다. 설사 WS-Security에 대한 정책이 변경된다 하더라도 소스 코드를 전혀 수정할 필요가 없는 것이다. 이클립스 개발환경에서 서비스 프로바이더 프로젝트(cxf-sample-project)를 톰켓 서버로 뛰운 상태에서 클라이언트 테스트 클래스를 JUnit으로 실행하면 console에 기록되는 메시지를 살펴보면 WS-Security 포맷의 SOAP 메시지로 변경되어 요청된다. WS-Security 요청 메시지 샘플
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"><soap:Header><wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" soap:mustUnderstand="1"><xenc:EncryptedKey xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Id="EncKeyId-EBDC91EE354299835912779728815005"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" /><ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<wsse:SecurityTokenReference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><ds:X509Data>
<ds:X509IssuerSerial>
<ds:X509IssuerName>CN=localhost</ds:X509IssuerName>
<ds:X509SerialNumber>1277784112</ds:X509SerialNumber>
</ds:X509IssuerSerial>
</ds:X509Data></wsse:SecurityTokenReference>
</ds:KeyInfo><xenc:CipherData><xenc:CipherValue>CvejtN49IQuTlqAwcYjR7vZ+vZgt21ysxsdwez35Iqb3NKGej7sjaoIvaCjsq4xnIooIhaXYcloksiEKijUUxN/EYQZgV2FxLwpO5uYi/9QgUqWrcwZqpjQuyLfDlVSM/18PeJmE0wAGRl1nGaCAAZlhEDB2tRhMJB49/xoh6p4=</xenc:CipherValue></xenc:CipherData><xenc:ReferenceList><xenc:DataReference URI="#EncDataId-4" /><xenc:DataReference URI="#EncDataId-5" /></xenc:ReferenceList></xenc:EncryptedKey><xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Id="EncDataId-4" Type="http://www.w3.org/2001/04/xmlenc#Element"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc" /><ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<wsse:SecurityTokenReference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><wsse:Reference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" URI="#EncKeyId-EBDC91EE354299835912779728815005" /></wsse:SecurityTokenReference>
</ds:KeyInfo><xenc:CipherData><xenc:CipherValue>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
kyB6VCR2k4E=</xenc:CipherValue></xenc:CipherData></xenc:EncryptedData><wsu:Timestamp xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="Timestamp-1"><wsu:Created>2010-07-01T08:27:59.078Z</wsu:Created><wsu:Expires>2010-07-01T08:32:59.078Z</wsu:Expires></wsu:Timestamp></wsse:Security></soap:Header><soap:Body xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="id-3"><xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Id="EncDataId-5" Type="http://www.w3.org/2001/04/xmlenc#Content"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc" /><ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<wsse:SecurityTokenReference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><wsse:Reference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" URI="#EncKeyId-EBDC91EE354299835912779728815005" /></wsse:SecurityTokenReference>
</ds:KeyInfo><xenc:CipherData><xenc:CipherValue>2NJaIKrZg4TEzBySTG3gM1NwXFTHKCVkj/pTTuzKGMoOA3R8baccX+/sp2HxhsdW4zfePCNDADsd
1+gQ3aTaifVmSz8eSH4JvasEgbHYN/u79Xtg0pJI6/ekCQMPQHcL7aZ2un3RQeEAcZU9NzfLIg==</xenc:CipherValue></xenc:CipherData></xenc:EncryptedData></soap:Body></soap:Envelope>
WS-Security 응답 메시지 샘플
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"><soap:Header><wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" soap:mustUnderstand="1"><xenc:EncryptedKey xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Id="EncKeyId-CD9ECFED812A2A37E512779728885785"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" /><ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<wsse:SecurityTokenReference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><ds:X509Data>
<ds:X509IssuerSerial>
<ds:X509IssuerName>CN=clientuser</ds:X509IssuerName>
<ds:X509SerialNumber>1277784127</ds:X509SerialNumber>
</ds:X509IssuerSerial>
</ds:X509Data></wsse:SecurityTokenReference>
</ds:KeyInfo><xenc:CipherData><xenc:CipherValue>egeNxBQ54X7WmGE9LdLD9FwjQyoMF9fdjIXXlg9b8KKntxBRFtjO3fb51rws0IAybz7E0F//Rd5oqg1tB9KNkSrnKL4No5Vz9X1LTXZPszcT4YL2kh5mK7WZaui8dw/k2aODOXQw0o4CNGN61O3IOHq8HiMXUJ0ItohuBJCdTzA=</xenc:CipherValue></xenc:CipherData><xenc:ReferenceList><xenc:DataReference URI="#EncDataId-4" /><xenc:DataReference URI="#EncDataId-5" /></xenc:ReferenceList></xenc:EncryptedKey><xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Id="EncDataId-4" Type="http://www.w3.org/2001/04/xmlenc#Element"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc" /><ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<wsse:SecurityTokenReference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><wsse:Reference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" URI="#EncKeyId-CD9ECFED812A2A37E512779728885785" /></wsse:SecurityTokenReference>
</ds:KeyInfo><xenc:CipherData><xenc:CipherValue>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
DEnoFDCIi4Y=</xenc:CipherValue></xenc:CipherData></xenc:EncryptedData><wsu:Timestamp xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="Timestamp-1"><wsu:Created>2010-07-01T08:28:08.515Z</wsu:Created><wsu:Expires>2010-07-01T08:33:08.515Z</wsu:Expires></wsu:Timestamp></wsse:Security></soap:Header><soap:Body xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="id-3"><xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Id="EncDataId-5" Type="http://www.w3.org/2001/04/xmlenc#Content"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc" /><ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<wsse:SecurityTokenReference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><wsse:Reference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" URI="#EncKeyId-CD9ECFED812A2A37E512779728885785" /></wsse:SecurityTokenReference>
</ds:KeyInfo><xenc:CipherData><xenc:CipherValue>SdmwkqGNhsspCdzDsKsUsHAfxr0xRTlYHt6r4n2HKTRvIIj7f8qhht7bNjSmqZTBDPV65irjtSVT
uGRyZERDEbSz5/3weCj6Wuag5y96HqruIRGxts4bexmZLAFZU+OaTjCAk/2t4jAMPHARk2S5/g==</xenc:CipherValue></xenc:CipherData></xenc:EncryptedData></soap:Body></soap:Envelope>

2010년 6월 29일 화요일

MTOM 가능한 웹서비스 개발(CXF)

Resources:
JAX-WS에서 MTOM 사용 가능

바이너리 파일 첨부를 사용하는 서비스에서 요청 객체에 DataHandler를 프로퍼티를 사용하여 바이너리 데이터를 전송할 수 있다. 해당 프로퍼티에 XmlMimeType을 "application/octet-stream"으로 지정한다.

요청 객체 샘플


package mypackage;

import javax.activation.DataHandler;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlMimeType;
import javax.xml.bind.annotation.XmlType;

@XmlAccessorType(XmlAccessType.PUBLIC_MEMBER)
@XmlType
public class UploadImeiFileRequest {

private String uploadUserId;


private DataHandler imeiFile;

public String getUploadUserId() {
return uploadUserId;
}
public void setUploadUserId(String uploadUserId) {
this.uploadUserId = uploadUserId;
}
@XmlMimeType("application/octet-stream")
public DataHandler getImeiFile() {
return imeiFile;
}
public void setImeiFile(DataHandler imeiFile) {
this.imeiFile = imeiFile;
}

}



Service Endpoint Interface


CXF에서는 BindingType을 어노테이션을 반드시 지정해 주어야 할 필요는 없지만 웹서비스를 명시적으로 MTOM 가능하도록 나타내도록 한다. 또한 MTOM 어노테이션을 사용하기도 한다.
package com.mycompany.ws;

import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;
import javax.xml.ws.BindingType;

import com.sds.shp.webservice.message.UploadImeiFileRequest;
import com.sds.shp.webservice.message.UploadImeiFileResponse;

@WebService(name="imeiFileWebService", 
serviceName="imeiFileWebService",
targetNamespace="http://mocompany.com/ws/fileupload")
@BindingType(javax.xml.ws.soap.SOAPBinding.SOAP11HTTP_MTOM_BINDING)  
@javax.xml.ws.soap.MTOM
@SOAPBinding(parameterStyle = SOAPBinding.ParameterStyle.BARE)  
public interface FileUploadWebService{

@WebMethod(operationName="uploadImeiFile")
@WebResult(name="UploadUserImageResponse")
UploadImeiFileResponse uploadImeiFile(@WebParam(name="UploadImeiFile") UploadImeiFileRequest request) throws Exception;


}



서비스 cxf 설정 파일


jaxws:endpoint의 jaxws:properties 의 "mtom-enabled" 속성을 "true"로 지정한다.

<jaxws:endpoint id="fileUploadService" 
implementor="com.mycompany.ws.FileUploadWebServiceImpl"
address="/fileUploadWebService">
<jaxws:properties>
<entry key="mtom-enabled" value="true"/>
</jaxws:properties>
</jaxws:endpoint>


클라이언트 샘플1. Dispatch 사용


CXF를 전혀 사용하지 않고 Java 표준 api만으로 파일 첨부 클라이언트를 구현한 예제다.

클라이언트 샘플2. CXF의 JaxWsProxyFactoryBean 사용


CXF 환경 설정 없이 SEI 만으로 클라이언트를 생성하여 사용한다.

클라이언트 샘플3. 스프링 빈 등록 후 클라이언트 사용


스프링 설정 파일에 클라이언트를 등록한 후 Injection 객체를 사용한다.

package com.sds.shp.webservice.server;

import java.io.File;
import java.net.URL;

import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.annotation.Resource;
import javax.xml.namespace.QName;
import javax.xml.soap.AttachmentPart;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPBodyElement;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPMessage;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.Dispatch;
import javax.xml.ws.Service;
import javax.xml.ws.soap.SOAPBinding;

import org.apache.cxf.common.util.SOAPConstants;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
import org.apache.cxf.transport.http.HTTPConduit;
import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import mypackage.LogManager;
import mypackage.PropertyNames;
import mypackage.UploadImeiFileRequest;
import mypackage.UploadImeiFileResponse;

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


String endpointAddress = "http://mycompany.com/endpointuri";

String nsURI = "http://mycompany.com/ws/fileupload";

@Test
public void testDispatch() throws Exception {
MessageFactory factory = MessageFactory.newInstance();

QName serviceName = new QName(nsURI, "imeiFileWebService");
QName portName = new QName(nsURI, "imeiFileWebServicePort");

Service service = Service.create(new URL(endpointAddress + "?wsdl"), serviceName);
service.addPort(portName, SOAPBinding.SOAP11HTTP_MTOM_BINDING, endpointAddress);

SOAPMessage soapRequest = factory.createMessage();
SOAPBody body = soapRequest.getSOAPBody();

QName uploadImeiFileQName = new QName(nsURI, "uploadImeiFile", "ime");
SOAPBodyElement payload = body.addBodyElement(uploadImeiFileQName);

QName inParamName = new QName("UploadImeiFile");
SOAPElement inParamElement = payload.addChildElement(inParamName);

QName imeiName = new QName("imeiFile");
SOAPElement imeiElement = inParamElement.addChildElement(imeiName);

QName xopIncludeName = new QName("http://www.w3.org/2004/08/xop/include", "Include", "xop");
SOAPElement includeElement = imeiElement.addChildElement(xopIncludeName);

String cid = "" + System.currentTimeMillis();
includeElement.addAttribute(new QName("href"), "cid:" + cid);

QName uploadUserIdName = new QName("uploadUserId");
SOAPElement uploadUserIdElement = inParamElement.addChildElement(uploadUserIdName);
uploadUserIdElement.setTextContent("xxxx");



Dispatch dispatch = service.createDispatch(portName, SOAPMessage.class, Service.Mode.MESSAGE);

BindingProvider bp = (BindingProvider) dispatch;
((SOAPBinding) bp.getBinding()).setMTOMEnabled(true);

DataSource ds = new FileDataSource("d:/test2.zip");
DataHandler dataHandler = new DataHandler(ds);

AttachmentPart attachmentPart = soapRequest.createAttachmentPart(dataHandler);
attachmentPart.setContentId(cid);
soapRequest.addAttachmentPart(attachmentPart);


//    System.out.println("=== SOAP REQUEST ===");
//    soapRequest.writeTo(System.out);
//    System.out.println("=====================");

long start = System.currentTimeMillis();

dispatch.invoke(soapRequest);

System.out.println("==========> elapsed: " + (System.currentTimeMillis() - start));
}

@Test
public void testClientProxyFactoryBean() throws Exception {
long start = System.currentTimeMillis();
JaxWsProxyFactoryBean factoryBean = new JaxWsProxyFactoryBean();

java.util.Map props = new java.util.HashMap();
props.put(SOAPConstants.MTOM_ENABLED, Boolean.TRUE.toString());

factoryBean.setProperties(props);
factoryBean.setServiceClass(IImeiFileWebService.class);
factoryBean.setAddress(endpointAddress);

IImeiFileWebService service = (IImeiFileWebService) factoryBean.create();


// start.

boolean useProxy = false;

if(useProxy) {
String httpProxyServer = "proxy-ip";
int httpProxyServerPort = 8080;
int httpConnectionTimeout = 30000;
boolean httpAllowChunking = false;


Client client = ClientProxy.getClient(service);
client.getEndpoint().put( "mtom-enabled", "true" );

if(LogManager.isDebugEnabled()) {
String ln = System.getProperty("line.separator");
StringBuffer sb = new StringBuffer(ln);

sb.append("###############################################").append(ln);
sb.append(PropertyNames.HTTP_PROXY_SERVER).append(": ").append(httpProxyServer).append(ln);
sb.append(PropertyNames.HTTP_PROXY_SERVER_PORT).append(": ").append(httpProxyServerPort).append(ln);
sb.append(PropertyNames.HTTP_CONNECTION_TIMEOUT).append(": ").append(httpConnectionTimeout).append(ln);
sb.append(PropertyNames.HTTP_ALLOW_CHUNKING).append(": ").append(httpAllowChunking).append(ln);
sb.append("###############################################").append(ln);

LogManager.debug(getClass(), sb.toString());
}

HTTPConduit http = (HTTPConduit) client.getConduit();
HTTPClientPolicy httpClientPolicy = new HTTPClientPolicy();
httpClientPolicy.setConnectionTimeout(httpConnectionTimeout);
httpClientPolicy.setAllowChunking(httpAllowChunking);
httpClientPolicy.setProxyServer(httpProxyServer);
httpClientPolicy.setProxyServerPort(httpProxyServerPort);
http.setClient(httpClientPolicy);

}   

// end.

UploadImeiFileRequest request = new UploadImeiFileRequest();
request.setUploadUserId("abc");

//    DataSource dataSource = new FileDataSource(new File("d:/test2.zip"));
DataSource dataSource = new FileDataSource(new File("d:/test2.zip"));
DataHandler dataHandler = new DataHandler(dataSource);

request.setImeiFile(dataHandler);

UploadImeiFileResponse response = service.uploadImeiFile(request);
System.out.println((System.currentTimeMillis() - start) + " milli seconds" );
}

@Resource(name="imeiFileClient")
IImeiFileWebService client;
@Test
public void testImeiFileWebServiceClient() throws Exception {
try {
long start = System.currentTimeMillis();
UploadImeiFileRequest request = new UploadImeiFileRequest();
request.setUploadUserId("xx");

DataSource dataSource = new FileDataSource(new File("d:/test2.zip"));
DataHandler dataHandler = new DataHandler(dataSource);

request.setImeiFile(dataHandler);

UploadImeiFileResponse response = client.uploadImeiFile(request);

System.out.println((System.currentTimeMillis() - start) + " milli seconds" );

} catch(Exception ex) {
ex.printStackTrace();
throw ex;
}
}

}



Injection된 객체를 사용하기 위해서는 스프링 설정파일에 아래와 같이 클라이언트를 등록해야 한다.

아래와 같이 클라이언트 빈을 지정해 주면 된다.
mtom-enabled속성을 true로 지정한다.
주의할 점은 endpointName과 serviceName을 namespace를 포함하여 반드시 지정하기를 권장한다. 개발 환경(개발자피씨)에서는 endpointName(PortName)와 serviceName을 지정하지 않아도 서비스와 연동하는데 문제가 되지 않지만, 외부 서버로 서비스를 디플로이 한 후에 연동하고자 할 때 문제가 발생한다.

<jaxws:client id="imeiFileClient" xmlns:tns="http://mocompany.com/ws/fileupload"
serviceClass="com.mycompany.ws.FileUploadWebService"
address="http://mycompany.com/ws/fileUploadWebService"
endpointName="tns:imeiFileWebServicePort"
serviceName="tns:imeiFileWebService">
<jaxws:properties>
<entry key="mtom-enabled" value="true"/>
</jaxws:properties>

</jaxws:client>




Sample Soap message


Payload: 
--uuid:08654ae5-c060-47dd-9bee-48dff0090a65
Content-Type: application/xop+xml; charset=UTF-8; type="text/xml";
Content-Transfer-Encoding: binary
Content-ID: <root.message@cxf.apache.org>

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ns2:uploadImeiFile 
xmlns:ns2="http://socialhub.samsungmobile.com/store/ws/imeifile" 
xmlns:ns3="http://socialhub.samsungmobile.com/store/ws">
<UploadImeiFile><imeiFile>
<xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include" 
href="cid:3381b3c2-d57e-490a-bccb-7c91487f24b2-1@http%3A%2F%2Fcxf.apache.org%2F"/></imeiFile>
<uploadUserId>xx</uploadUserId></UploadImeiFile>
</ns2:uploadImeiFile></soap:Body>
</soap:Envelope>
--uuid:08654ae5-c060-47dd-9bee-48dff0090a65
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary
Content-ID: <3381b3c2-d57e-490a-bccb-7c91487f24b2-1@http://cxf.apache.org/>

0? ?0? ??      K? ?0   *?H?8   

--uuid:08654ae5-c060-47dd-9bee-48dff0090a65--

2010년 6월 23일 수요일

CXF 에서 HttpProxy 설정

프로그래밍으로 설정



package com.sds.shp.cxf.util;

import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
import org.apache.cxf.transport.http.HTTPConduit;
import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;

import com.sds.shp.cxf.interceptor.SevenClientHandler;

public class ServiceClientUtil {
  @SuppressWarnings("unchecked")
  public static <T> T createClientProxy(Class<T> serviceEndpointInterface, String endpointUri) {
    
    JaxWsProxyFactoryBean proxyFactoryBean = new JaxWsProxyFactoryBean();
    
    proxyFactoryBean.setServiceClass(serviceEndpointInterface);
    proxyFactoryBean.setAddress(endpointUri);

    T service = (T) proxyFactoryBean.create();
    Client client = ClientProxy.getClient(service);
    
    String proxyServer = "serverip";
    int proxyServerPort = 10000;
    
    HTTPConduit http = (HTTPConduit) client.getConduit();
    HTTPClientPolicy httpClientPolicy = new HTTPClientPolicy();
    httpClientPolicy.setConnectionTimeout(36000);
    httpClientPolicy.setAllowChunking(false);
    httpClientPolicy.setProxyServer(proxyServer);
    httpClientPolicy.setProxyServerPort(proxyServerPort);
    http.setClient(httpClientPolicy);


    return service;
  }
}




Resources:
http://markmail.org/message/fir6oi62eivaoxuu

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월 5일 월요일

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);
}

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);
}

}

}


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>

2010년 3월 26일 금요일

CXF 웹 서비스 개발

1. web.xml


web.xml 파일에서는 cxf를 실행할 수 있는 환경을 설정해 주어야 한다.
아래의 샘플 파일에서는 스프링 설정 파일로 WEB-INF/beans.xml 파일을 설정하고 있다.
또한 웹 경로의 /services/* 로 시작하는 URL 은 CXFServlet으로 제어를 넘긴다.

web.xml

----------------------------------------------------------
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>WEB-INF/beans.xml</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<servlet>
<servlet-name>CXFServlet</servlet-name>
<display-name>CXF Servlet</display-name>
<servlet-class>
org.apache.cxf.transport.servlet.CXFServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>CXFServlet</servlet-name>
<url-pattern>/services/*</url-pattern>
</servlet-mapping>
</web-app>

----------------------------------------------------------


2. beans.xml


beans.xml 파일에서는 CXF에서 실행할 웹서비스 정보를 설정해 주면 된다.
아래의 경우에 /services/OrderProcess 라고 요청이 들어오면 demo.order.OrderProcessImpl
클래스의 특정 메서드가 실행된다.

beans.xml
----------------------------------------------------------

<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="orderProcess"
implementor="demo.order.OrderProcessImpl"
address="/OrderProcess" />
</beans>
----------------------------------------------------------



3. Endpoint Service Interface


Interface : OrderProcess


----------------------------------------------------------
package demo.order;

import javax.jws.WebMethod;
import javax.jws.WebService;

@WebService
public interface OrderProcess {

@WebMethod
String processOrder(Order order);
}
----------------------------------------------------------


4. Service 구현 클래스



----------------------------------------------------------
package demo.order;

import javax.jws.WebMethod;
import javax.jws.WebService;

@WebService()
public class OrderProcessImpl implements OrderProcess {

@Override
public String processOrder(Order order) {
String orderID = validate(order);
return orderID;
}
@WebMethod(action="1", exclude=false)
private String validate(Order order) {
String custID = order.getCustomID();
String itemID = order.getItemID();
int qty = order.getQty();
double price = order.getPrice();

if(custID != null && itemID != null && !custID.equals("") &&
!itemID.equals("") && qty > 0 && price > 0.0) {
return "ORD1234";
}
return null;
}

}
----------------------------------------------------------


5. Soap Message


Soap Body로 리턴되는 클래스는 XmlRootElement annotation을 반드시 선언해 주어야 한다.

----------------------------------------------------------
package demo.order;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "Order")
public class Order {
private String customID;
private String itemID;
private int qty;
private double price;

public Order() {
super();
}

public String getCustomID() {
return customID;
}

public void setCustomID(String customID) {
this.customID = customID;
}

public String getItemID() {
return itemID;
}

public void setItemID(String itemID) {
this.itemID = itemID;
}

public int getQty() {
return qty;
}

public void setQty(int qty) {
this.qty = qty;
}

public double getPrice() {
return price;
}

public void setPrice(double price) {
this.price = price;
}
}
----------------------------------------------------------


6. 결과


이렇게 구성된 web application을 cxfsample-web.war 파일로 묶어서 정상적으로 톰켓에 디플로이 시켰다면,
http://localhost:8080/cxfsample-web/services/OrderProcess?wsdl 라고 웹브라우저의 주소창에 입력하였다면 화면에서 본 서비스의 WSDL 내용을 볼 수 있다.

7. 테스트


Soap 메시지 기반의 웹서비스 테스트는 SoapUI를 이용해서 테스트 할 수 있다.

REQUEST MESSAGE SAMPLE

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ord="http://order.demo/">
<soapenv:Header/>
<soapenv:Body>
<ord:processOrder>
<arg0>
<customID>u111</customID>
<itemID>a111</itemID>
<price>100</price>
<qty>10</qty>
</arg0>
</ord:processOrder>
</soapenv:Body>
</soapenv:Envelope>


RESPONSE MESSAGE Sample

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ns2:processOrderResponse xmlns:ns2="http://order.demo/">
<return>ORD1234</return>
</ns2:processOrderResponse>
</soap:Body>
</soap:Envelope>

2010년 3월 10일 수요일

Eclipse(galileo)에 CXF 플러그인 설치

Eclipse 3.6 (Helios)에서는 이런 고민 필요없다.
CXF가 기본적으로 제공된다.



-- 이건 옛날 고민이 되어 버렸다.

웹서비스 플랫폼을 선택할 때 Axis2와 CXF 중에서 어떤 걸 선택할 지를 고민을 꽤 많이했다. 사실 그 고민은 아직도 진행중이긴 하다.
둘다 Apache Software Foundation 에서 관리하는 프로젝트 이긴 한데, 어떤 걸 선택할 때 참고가 될 지 모르겠지만 웹 블로그 하나를 소개하도록 하겠다.
http://www.theserverside.com/tt/articles/article.tss?l=AxisAxis2andCXF

위의 글도 2007년도에 작성되었기 때문에 두 프로젝트를 비교한 factor가 현재 시점에서 보면 맞지 않는 부분이 있는데, 그 이유는 버전이 올라가면서 둘간의 장점은 상쇄되고, 단점이 보완되어 가기 때문이다. 2009년 가을까지만 해도 Axis2가 JAX-RS를 지원하지 않아 CXF를 선택하는 데, 주저함이 없었지만 Axis2 1.5에서 Restful을 지원한다.

Axis2가 Spring가 통합되지 않는 건 아니지만, CXF는 Spring 기반으로 작성되어 있어 Spring 기반 프로젝트라면 좀 더 사용하기가 용이할 거라 생각한다.

Axis2의 장점이라면 Eclipse의 웹서비스 툴로써 기본적으로 Axis가 포함되어 있기 때문에 개발 시 별도의 플러그인을 설치할 필요가 없다는 것이다.

CXF 플러그인 WTP의 incubation으로 제공되고 있으며 개발자가 플러그인을 설치해 주어야 한다.

CXF 플러그인 설치
Help --> Install New Software... 메뉴를 클릭한 후 URL에 http://download.eclipse.org/webtools/updates 라고 입력하면 CXF Web Services 라는 플러그인을 찾을 수 있다. 선택하여 설치해 주어야 한다.




CXF 설치
http://cxf.apache.org/download.html 에서 CXF를 다운받아 로컬 디스크의 특정 위치에 압축을 푼다. (ex: d:\apache\cxf\v2.2.6)

Preference 설정





Resources:
http://www.eclipse4sl.org/documentation/userdoc/html/webservice/cxf/index.php
http://www.eclipse.org/webtools/