2

I'm using Spring's 'HTTP Invoker' remoting solution to expose DAOs to many different applications, but have all database access in a single server.

This works well, but if the server throws, say, a HibernateSystemException, Spring serializes that and sends it over the wire back to the client. That doesn't work because the client doesn't (and shouldn't) have HibernateSystemException in its classpath.

Might there be a way to have Spring Remoting wrap my exception in something that I specify that would be common between client and server to avoid issues like this?

I know that I could do that in my server code by wrapping everything the DAO does in a try/catch, but that's admittedly sloppy.

Thanks, Roy

Roy Truelove
  • 22,016
  • 18
  • 111
  • 153

5 Answers5

4

I ran into this issue as well; I am exposing a service via HTTP Invoker that accesses a database using Spring 3.1, JPA 2, and Hibernate as the JPA provider.

To work around the problem, I wrote a custom Interceptor and an exception called WrappedException. The interceptor catches exceptions thrown by the service and converts the exceptions and causes to WrappedException using reflection and setters. Assuming the client has the WrappedException on its class path, the stack trace and original exception class names are visible to the client.

This relaxes the need for the client to have Spring DAO on its class path and as far as I can tell, no original stack trace information is lost in the translation.

Interceptor

public class ServiceExceptionTranslatorInterceptor implements MethodInterceptor, Serializable {

    private static final long serialVersionUID = 1L;

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        try {
            return invocation.proceed();
        } catch (Throwable e) {
            throw translateException(e);
        }
    }

    static RuntimeException translateException(Throwable e) {
        WrappedException serviceException = new WrappedException();

        try {
            serviceException.setStackTrace(e.getStackTrace());
            serviceException.setMessage(e.getClass().getName() +
                    ": " + e.getMessage());
            getField(Throwable.class, "detailMessage").set(serviceException, 
                    e.getMessage());
            Throwable cause = e.getCause();
            if (cause != null) {
                getField(Throwable.class, "cause").set(serviceException,
                        translateException(cause));
            }
        } catch (IllegalArgumentException e1) {
            // Should never happen, ServiceException is an instance of Throwable
        } catch (IllegalAccessException e2) {
            // Should never happen, we've set the fields to accessible
        } catch (NoSuchFieldException e3) {
            // Should never happen, we know 'detailMessage' and 'cause' are
            // valid fields
        }
        return serviceException;
    }

    static Field getField(Class<?> clazz, String fieldName) throws NoSuchFieldException {
        Field f = clazz.getDeclaredField(fieldName);
        if (!f.isAccessible()) {
            f.setAccessible(true);
        }
        return f;
    }

}

Exception

public class WrappedException extends RuntimeException {

    private static final long serialVersionUID = 1L;

    private String message = null;

    public void setMessage(String message) {
        this.message = message;
    }

    @Override
    public String toString() {
        return message;
    }
}

Bean Wiring

<bean id="exceptionTranslatorInterceptor" class="com.YOURCOMPANY.interceptor.ServiceExceptionTranslatorInterceptor"/>

<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="beanNames" value="YOUR_SERVICE" />
    <property name="order" value="1" />
    <property name="interceptorNames">
        <list>
            <value>exceptionTranslatorInterceptor</value>
        </list>
    </property>
</bean>
user1366367
  • 141
  • 1
  • 1
  • 5
  • Perfect, that's what I was looking for. After sitting with this issue for a while I figured that I'd need an interceptor to handle it but never coded. Thanks! – Roy Truelove Apr 30 '12 at 18:07
0

I used a solution similar to N1H4L's but with AspectJ.

First I made all the exceptions I want the client to be aware of to extend the class BusinessException (which in my case is a very simple subclass of RuntimeException in the jar with the service interface and DTOs).

Since I don't want the client to know much about the internals of the service I just say "Internal server error".

package com.myproduct.myservicepackage;

import com.myproduct.BusinessException;
import org.aspectj.lang.*;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class InternalServerErrorExceptionAspect {
    @Pointcut("execution(public * com.myproduct.myservicepackage..*Service.*(..))")
    public void publicServiceMethod() {}

    @Around("publicServiceMethod()")
    public Object hideNonBusinessExceptions(ProceedingJoinPoint jp) throws Throwable {
        try {
            return jp.proceed();
        } catch (BusinessException e) {
            throw e;
        } catch (RuntimeException e) {
            e.printStackTrace();
            throw new RuntimeException("Internal server error.")
        } 
    }
}

Here's the BusinessException class:

package com.myproduct.BusinessException;

public class BusinessException extends RuntimeException {

    private static final long serialVersionUID = 8644864737766737258L;

    public BusinessException(String msg) {
        super(msg);
    }

}
Douglas Mendes
  • 322
  • 2
  • 12
0

I was using AspectJ to wrap exception but it does not work for exception that occur in Spring proxy, e.g. annotation @Transactional when the connection to the database fails. However, the method setInterceptor on RmiServiceExporter works perfectly.

JDM
  • 83
  • 1
  • 6
0

I can understand you don't want your clients to have HibernateSystemException in their classpath, but I'd argue they should, if you're using HTTPInvoker appropriately. It's not designed to be a service facade / interface layer: all it's meant to do is let you run Java methods on a remote JVM, using HTTP instead of RMI.

So if you really don't want the clients to have a dependency on Hibernate, your try/catch block is the way to go. (I'd argue against that though, since it'll make debugging a pain: your stack trace will now be divided between the client and the server).

I haven't used it myself but you could try the org.springframework.remoting.support.RemoteExporter.setInterceptors(Object[]) method to add an aspect to catch that particular exception in just one place rather than adding try/catches all over the place.

artbristol
  • 32,010
  • 5
  • 70
  • 103
0

I would argue a try/catch in a Facade layer in front of your DAOs is exactly what you want, in order to gain full control over the exceptions you return. I agree it initially feels ugly, but it's a vital layer between client and DAO, in my opinion.

You might even return a OperationStatus object of some sort, rather than use void return types, to convey both outcome (worked, didn't) and error message, for store-data API calls.

Brian
  • 6,391
  • 3
  • 33
  • 49