14

I want to use a static method as setter helper that catch exceptions and print debug info about the operation that failed. I don't want the exception details only. I want to show what property was being set so that detail help to debug the problem quickly. I am working with Java 8.

How should I provide or detect the property being set?

What I wish is to remove the "name" string in the example and get the same result.

I know I can't use reflection over the supplied setter method supplied that is transformed to lambda expression and then to BiConsumer.

I got this but the property name needs to be provided.

/** setter helper method **/
private static <E, V> void set(E o, BiConsumer<E, V> setter,
        Supplier<V> valueSupplier, String propertyName) {
    try {
        setter.accept(o, valueSupplier.get());
    } catch (RuntimeException e) {
        throw new RuntimeException("Failed to set the value of " + propertyName, e);
    }
}

Example:

    Person p = new Person();
    Supplier<String> nameSupplier1 = () ->  "MyName";
    Supplier<String> nameSupplier2 = () -> { throw new RuntimeException(); };
    set(p, Person::setName, nameSupplier1, "name");
    System.out.println(p.getName()); // prints MyName
    set(p, Person::setName, nameSupplier2, "name"); // throws exception with message  Failed to set the value of name
    System.out.println(p.getName()); // Does not execute

EDIT: I know reflection does not help with the lambdas. I know AOP and I know this can be made with pure reflection too but I want to know if there a better way to get this done with Java 8 that didn't exist with Java 7. It seems it should to me. Now it is possible to do things like to pass a setter method to another one.

aalku
  • 2,860
  • 2
  • 23
  • 44
  • I don't like the question title. Suggestions or edits are welcome. – aalku Feb 18 '14 at 17:20
  • I'm not sure if this is helpfull, but maybe it will help with debug info: https://code.google.com/p/java-interceptor/wiki/Documentation – Zavior Feb 18 '14 at 17:28
  • Thank you @Zavior but I want something less intrusive, not intercepting every call but capturing the error and working only in case of error. – aalku Feb 18 '14 at 17:34
  • Aspect Oriented Programming was intended for this purpose. I suggest AspectJ (or the google interceptor). you cannot specify a property with the `::` format because that is just compiled into an anonymous class, without any underlying information. – aepurniet Feb 18 '14 at 17:55
  • the alternative is to use a reflective approach, get rid of the `::`, and lookup the property setting method based on the name. – aepurniet Feb 18 '14 at 17:56
  • How can I do this with AOP? I would need to capture a fail in an expression and in the exception name the setter method (propery) where I was going to store the expression value. When the expression fails the setter wasn't called yet. This is why I thought a lambda expression would help, because the expression can be evaluated inside the helper method. – aalku Feb 18 '14 at 21:48
  • @aepurniet A reference to a method object can't be get without naming it with a string, that is what I tried to avoid. That's the only problem with my existing code. – aalku Feb 18 '14 at 21:50
  • 1
    That’s a strange question. How does it help solving a problem to print a property name when it wasn’t the property but the `Supplier` that failed? If it is the property which fails, its setter method will be included in the stack trace. If it’s not, well, focus on the thing that failed rather than the uninvolved property. – Holger Feb 19 '14 at 10:10
  • 1
    @Holger It gives you a context that is very useful when is not the code that fails but the input data. If the data is wrong then the processing of it may fail and the wrong data is not enough context to debug, you need to know what is that data supossed to be, for example if it is the person name or the person phone number. The supplier may be a simple lambda like `s -> s.substring(1)`. – aalku Feb 19 '14 at 10:32

2 Answers2

24

In case you expect method references as the only input, you can debug them to printable names with the following trick:

public static void main(String[] args) {
  Person p = new Person();
  Supplier<String> nameSupplier1 = () -> "MyName";
  Supplier<String> nameSupplier2 = () -> { throw new RuntimeException(); };
  set(p, Person::setName, nameSupplier1);
  System.out.println(p.getName()); // prints MyName
  set(p, Person::setName, nameSupplier2); // throws exception with message
  System.out.println(p.getName()); // Does not execute
}

interface DebuggableBiConsumer<A, B> extends BiConsumer<A, B>, Serializable {}

private static <E, V> void set(
    E o, DebuggableBiConsumer<E, V> setter, Supplier<V> valueSupplier) {
  try {
    setter.accept(o, valueSupplier.get());
  } catch (RuntimeException e) {
    throw new RuntimeException("Failed to set the value of "+name(setter), e);
  }
}

private static String name(DebuggableBiConsumer<?, ?> setter) {
  for (Class<?> cl = setter.getClass(); cl != null; cl = cl.getSuperclass()) {
    try {
      Method m = cl.getDeclaredMethod("writeReplace");
      m.setAccessible(true);
      Object replacement = m.invoke(setter);
      if(!(replacement instanceof SerializedLambda))
        break;// custom interface implementation
      SerializedLambda l = (SerializedLambda) replacement;
      return l.getImplClass() + "::" + l.getImplMethodName();
    }
    catch (NoSuchMethodException e) {}
    catch (IllegalAccessException | InvocationTargetException e) {
      break;
    }
  }
  return "unknown property";
}

The limitations are that it will print not very useful method references for lambda expressions (references to the synthetic method containing the lambda code) and "unknown property" for custom implementations of the interface.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Good idea. Do you thin this will be compatible with non-oracle JRE? – aalku Feb 19 '14 at 11:57
  • I like it because all the ugly code may be hidden and upper level code may be as clean as I wanted. I have to test it. It looks very 'slow' but only in case of exception. – aalku Feb 19 '14 at 11:58
  • I can't make it work with my current Java 8 build (build 1.8.0-ea-b118). It can't find writeReplace method in the lambda. – aalku Feb 19 '14 at 12:09
  • 2
    Well `SerializedLambda` is *the* form of keeping compatibility between different implementations. Only the `writeReplace` method could be replaced with a different mechanism theoretically. If you want to be completely safe you have to write the object for real, e.g. into a byte array and parse the output. The output *must* contain a `SerializedLambda` for conforming implementations. And yes, it’s slow in the exception case only. – Holger Feb 19 '14 at 12:09
  • `java.lang.NoSuchMethodException: prueba2.X$$Lambda$37/933699219.writeReplace()` and exits the name method with "unknown property" – aalku Feb 19 '14 at 12:12
  • 1
    Which compiler did you use? I discovered that the current eclipse beta does not compile it correctly. I used netbeans and jdk-8-fcs-bin-b128. But I tested it now with several versions. Works with `netbeans` and/or `javac`. For eclipse we have to wait for a newer version… – Holger Feb 19 '14 at 12:17
  • It declares `private final void prueba2.X$$Lambda$37/280884709.writeObject(java.io.ObjectOutputStream) throws java.io.NotSerializableException`. Maybe with a custom ObjectOutputStream... – aalku Feb 19 '14 at 12:19
  • 1
    No, the problem is that the eclipse compiler does not generate a `Serializable` lambda where it should. The `writeObject` method will just throw a `NotSerializableException`. – Holger Feb 19 '14 at 12:21
  • I am compiling with eclipse. – aalku Feb 19 '14 at 12:22
  • As you said `java.io.NotSerializableException: Non-serializable lambda` – aalku Feb 19 '14 at 12:31
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/47858/discussion-between-user270349-and-holger) – aalku Feb 19 '14 at 12:48
  • 2
    I filed a report to the eclipse team; let’s see how long the fix will take. It might turn out to be an easy fix as `Serialization` for lambda expressions (`(…) -> …`) already works. – Holger Feb 19 '14 at 17:09
  • 1
    The `Serialization` of method references has been fixed now in a way that it is possible to (de)serialize them but differently to `javac` so this code here won’t print the desired method name, but I keep track of this issue. – Holger Feb 25 '14 at 17:36
  • I guess it is the best or the only that can be get with Java 8. I hope they allow `Method` static references like `X.class` class references some day. Thank you. – aalku Feb 25 '14 at 20:28
  • 3
    [`MethodHandle`](http://download.java.net/jdk8/docs/api/java/lang/invoke/MethodHandle.html) is very close to that. On the byte code level they can be used like constants (like `Class` or `String` literals) already. And with Java 8 you can [decode a direct handle](http://download.java.net/jdk8/docs/api/java/lang/invoke/MethodHandles.Lookup.html#revealDirect-java.lang.invoke.MethodHandle-). By the way, this is used by the lambda expression/method reference implementation internally. – Holger Feb 25 '14 at 20:47
  • I think it will be close enough if static analysis tools (eg: eclipse, SonarQube) recognize the method being referenced. – aalku Feb 25 '14 at 21:18
  • Here's a non-broken link for that API on Java 8 to decode direct method handles; thanks for the great tip @Holger! https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/MethodHandles.Lookup.html#revealDirect-java.lang.invoke.MethodHandle- – Cagatay May 19 '15 at 21:16
1

What about that?

Source code:

package test;

import java.io.Serializable;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.function.Function;

public class Person {
    public String getName() {
        return "Michael";
    }

    public static void main(String[] args) throws
            NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Serializable s = (Function<Person, String> & Serializable) Person::getName;

        Method method = s.getClass().getDeclaredMethod("writeReplace");
        method.setAccessible(true);
        SerializedLambda serializedLambda = (SerializedLambda) method.invoke(s);

        System.out.println(serializedLambda.getImplClass().replace("/", ".")
                + "::" + serializedLambda.getImplMethodName());
    }
}

Output:

test.Person::getName

You actually need to look up the method "writeReplace", call it and evaluate its return value which is of type SerializedLambda.

mmirwaldt
  • 843
  • 7
  • 17