Handling stack traces with unknown exception classes
Asked Answered
S

3

8

I'm implementing a session bean that throws ApplicationExceptions. These exceptions have chained stack traces that may contain exceptions whose classes aren't available on the client. Something like:

@Override
public void doSomethingSpecial(MyObject o) throws MyException {
    try {
        legacySystem.handle(o);
    } catch (LegacyException e) {
        logger.warn(e.getMessage(), e);
        throw new MyException(e);
    }
}

Here it's possible that the client gets an exception it doesn't have the class for. This can result in:

Exception in thread "main" java.lang.reflect.UndeclaredThrowableException
    at sun.proxy.$Proxy0.doSomethingSpecial(Unknown Source)
    at com.myapp.client.Client.main(Client.java:56)
Caused by: java.lang.ClassNotFoundException: MyLegacyException

I don't want the client to know all the possible exceptions that can be thrown on the server side, but having a stack trace is never bad.

How do you handle these problems? Is it a passable solution to implement an Interceptor that decouples the stack trace when the exception is sent back to the client? But then the Interceptor should handle only calls via the RemoteInterface, because internally I'm interested in the whole stack trace.

Snuggle answered 20/9, 2013 at 7:58 Comment(2)
The design is wrong. Throwing exceptions that the client doesn't know about is completely pointless.Kandacekandahar
Mhh, but in a huge enterprise application I cannot know all the clients and the problem is not only the thrown Exception. The Problem is that there might be unknown exceptions in the stack trace too.Snuggle
I
1

It depends on your client type. If a client is another team which is developing another component or subsytem, I'm agree with you about:

Having a stack trace is never bad

But if they are customers who have no idea about your application internals, so there is no reason for them to know your exception classes or even see your stack traces. It would be nice to have a protocol which force you to catch all exceptions and wrap them in a high level exception class with a error_code property. This way, you can have a specific error code for each catch statement in your application and you will give your clients a list of these codes.

Anyway, from technical view, if your clients doesn't have access to your internal Exception classes, so they can't have access to your stack trace without referred ClassNotFoundException. If you really want them to see the stack trace, one solution could be to have an Aspect which sits just on the most upper layer of your API (which is going to be called by clients) and catches all the exceptions, writes their stack traces in a String and sends this as a property of the final exception which is going to be caught by caller. This way, the caller can access the stack trace as a formatted String property of the exception.

Edit:

You can even configure your build script, so that this Aspect never be a part of your release versions. So you can give this stack trace messages just in your debug version.

Infidelity answered 20/9, 2013 at 8:33 Comment(0)
G
0

I thought about little roundabout solution, but this is only untested speculation.

You initialize your external exception with internal exception. But if we look at javadoc of Throwable we can see methods get and setStackTrace(StackTraceElement[] stackTrace)

StackTraceElement is initialized with strings. So maybe you can get stack trace from internal exception and set it into your external exception (MyException).

Gallicanism answered 20/9, 2013 at 8:36 Comment(3)
You can do externalException.setStackTrace(internalException.getStackTrace()). But that doesn’t help if the client does not even know the external exception type.Fryer
You are right, but I expect client to know external exception, and the problem being in UndeclaredThrowableException. Which was caused by the fact we initialized external exception with internal exception here - throw new MyException(e);// e being LegacyExceptionGallicanism
Indeed, setting the stack trace this way might solve that specific problem completely.Fryer
F
0

Since RMI settles on Serialization you can use Serialization features to conditionally replace exceptions.

import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;

public class CarryException extends RuntimeException implements Serializable
{
  final String exceptionClass;

  public CarryException(Exception cause)
  {
    super(cause.getMessage());
    exceptionClass=cause.getClass().getName();
    setStackTrace(cause.getStackTrace());
  }

  @Override
  public String getMessage()
  {
    // if we get here, reconstructing the original exception did not work
    return exceptionClass+": "+super.getMessage();
  }

  /** Invoked by Serialization to get the real instance */
  final Object readResolve() throws ObjectStreamException
  {
    try
    {
      Exception ex = Class.forName(exceptionClass).asSubclass(Exception.class)
        .getConstructor(String.class).newInstance(super.getMessage());
      ex.setStackTrace(getStackTrace());
      return ex;
    }
    catch(InstantiationException|IllegalAccessException|ClassNotFoundException
      | IllegalArgumentException|InvocationTargetException|NoSuchMethodException
      | SecurityException ex)
    {
      // can't reconstruct exception on client side
    }
    return this; // use myself as substitute
  }
}

Now you can throw any exception to the client by throw new CarryException(originalException);. The CarryException will always record the stack trace and message of the original exception and recreate the original exception at client side if the class is available. Otherwise the client sees the CarryException so obviously that one exception type must be known on the the client side.

The exception type must have the standard constructor taking a message String for the reconstruction to work. (All other things would be too complicated). But most exception types have that.

There is another catch: replacing via Serialization does only work if Serialization is involved so you must not invoke the methods on the implementation class directly when beeing inside the same JVM. Otherwise you see the CarryException unconditionally. So you have to use a stub even locally, e.g.

((MyRemoteInterface)RemoteObject.toStub(myImplementation)).doSomethingSpecial();

Update

If MyException is known to the client and only LegacyException is not, of course the following works:

catch (LegacyException e) {
    logger.warn(e.getMessage(), e);
    MyException me=new MyException(e.toString());
    me.setStackTrace(e.getStackTrace());
    throw me;
}
Fryer answered 20/9, 2013 at 8:41 Comment(4)
Should not be a Problem as locally I always want the stack trace.Snuggle
You always get the right stack trace with this, even remote. It’s only about getting the right exception type which works if transported via Serialization and if the exception type is available at the receiving side.Fryer
I Think having a fake exception class (like CarryException) for all unknown exceptions couldn't be so useful.Infidelity
@ moghaddam: You think, getting an UndeclaredThrowableException for all unknown exceptions is better? Btw. that “fake” exception is not even necessary for the specific question here. But it could be useful when using like new MyException(new CarryException(legacyException));. The client still sees the exception it knows.Fryer

© 2022 - 2024 — McMap. All rights reserved.