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 버전을 올려 보시길...

2011년 2월 14일 월요일

JAXB 개발 환경에서 XSD 생성 (maven pom 파일)

Jaxb 관련 annotation을 이용하여 웹서비스를 개발하다보면
가끔씩 최종 XSD를 확인해 보고 싶은 경우가 생긴다.

물론 CXF와 같은 프레임워크를 이용하여 웹서비스를 개발하면 웹서비스 URI에 ?wsdl을 붙히면 WSDL을 살펴 볼 수도 있지만
개발 환경에서 @XmlRootElement, @XmlElement로 작업할 경우 매핑되는 XSD를 보고자 한다면 아래의 pom.xml 파일을 참고하기 바란다.

jdk1.6 (mustang)의 bin 폴더에 있는 xjc (xsd를 이용하여 java class 생성) 와 schemagen을 이용해도 동일한 결과를 얻을 수 있으나 maven plugin을 이용하는 편이 훨씬 수월하다.

<project xmlns="http://maven.apache.org/POM/4.0.0" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="
    http://maven.apache.org/POM/4.0.0 
    http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.archnal.maven</groupId>
  <artifactId>jaxb-project</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>jaxb-project</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>javax.xml.bind</groupId>
      <artifactId>jaxb-api</artifactId>
      <version>2.0</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.5</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>2.0.1</version>
      <type>jar</type>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <repositories>
    <repository>
      <id>java.net</id>
      <name>java.net Maven Repository</name>
      <url>https://maven-repository.dev.java.net/nonav/repository</url>
      <layout>legacy</layout>
    </repository>
  </repositories>

  <build>

    <plugins>
      <plugin>
        <groupId>com.sun.tools.jxc.maven2</groupId>
        <artifactId>maven-jaxb-schemagen-plugin</artifactId>
        <version>1.2</version>
        <executions>
          <execution>
            <phase>generate-resources</phase>
            <goals>
              <goal>generate</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <includes>
            <include>**/com/archnal/sample/jaxb/**</include>
          </includes>
          <verbose>true</verbose>
          <source>1.6</source>
        </configuration>
      </plugin>
      <plugin>
        <groupId>com.sun.tools.xjc.maven2</groupId>
        <artifactId>maven-jaxb-plugin</artifactId>
        <executions>
          <execution>
            <goals>
              <goal>generate</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <includeSchemas>
            <includeSchema>**/*.xsd</includeSchema>
            <includeSchema>**/*.dtd</includeSchema>
          </includeSchemas>
          <verbose>true</verbose>
          <source>1.6</source>
        </configuration>
      </plugin>

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
    </plugins>

  </build>
  <pluginRepositories>
    <pluginRepository>
      <id>maven-repository.dev.java.net</id>
      <name>Java.net Maven 2 Repository</name>
      <url>http://download.java.net/maven/2</url>
    </pluginRepository>
  </pluginRepositories>
</project>