gad_gadskiy
gad_gadskiy

Reputation: 220

Java annotation to change null value

I want to write an annotation that can change the parameter value to some default if it is null. For example:

static void myMethod(@NullToDefault String param) {
    System.out.println(param);
}

public static void main(String[] args) {
    myMethod("foo"); //prints "foo"
    myMethod(null);  //prints "default"
}

I have separately an annotation and a method to change the null value. But I can't get how to force the annotation to call this method by default. What should I do to complete this?

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@interface NullToDefault {
}

public static void inject(Object instance) { //how to call this automatically?
    Field[] fields = instance.getClass().getDeclaredFields();
    for (Field field : fields) {
        if (field.isAnnotationPresent(NullToDefault.class)) {
            field.setAccessible(true);
            try {
                if (field.get(instance) == null) {
                    field.set(instance, "default");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

Upvotes: -1

Views: 288

Answers (1)

rzwitserloot
rzwitserloot

Reputation: 103893

This is not directly possible. But there are tricks.

Annotation Processor

Annotation Processors (APs) have an API that limits them to making new source files, they can't edit existing ones. Hence, 'write an annotation processor that injects code that replaces a null value with some default' is not possible.

But, there are alternatives that get close.

You can set up your annotation system so that the users (i.e. the programmers using your annotation) write a 'template' in java and that your AP sees this and generates the real source file. APs can only 'see' signatures (the name, return type, parameter types, parameter names, and throws clauses of methods, and the name + type of fields, and a list of inner types, but not initializer expressions of fields, or the body of method declarations).

This does allow you to do things like this:

// This is a file you write
@SomeAnno
public class FooDefinition {
  private final String name;

  public static void greet(Foo self) {
    System.out.println("Hello, " + self.getName());
  }
}
// and this is what your AP can generate from that
public class Foo {
  private final String name;

  public Foo(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }

  public void greet() {
    FooDefinition.greet(this);
  }
}

This is a hypothetical AP that adds getters and a constructor. It 'works' because all you need to know from FooDefinition to generate Foo, is that final String name; exists, and that static void greet(Foo self) exists. You don't need to know the content of that greet method (as you can't).

This isn't exactly 'nice' as an experience: Foo doesn't exist until you do a full compilation run, and any modifications to FooDefinition will not affect Foo until you do another full build run. You will not get the benefit of making a quick change in your IDE and seeing this instantly reflected throughout your source code. I'm not sure it's a useful trick for your needs here - at best, you can make a wrapper clone of your class, where the body of each method in the code you generate replaces any nulled arguments with their defaults and then calls your code.

Agents

Agents are debug aids that 'plug in' to your JVM's core functionality and can modify things on the fly. In particular, they get to witness raw bytecode (in byte[] form, as in, a direct dump of the contents of a .class file) and can modify it if they want. You register them on boot (java -javaagent:/path/to/some.jar) - you can make one that sees the annotation and meddles with the bytecode to inject a default-replacer.

It's a bit odd in the sense that you have to have a runtime component (that agent, which must be specified with a -javaagent: parameter.

It's definitely possible and essentially 'live', i.e. in IDEs and such you just make an edit, save, and e.g. eclipse's hot code replace will work fine here, as your agent is still there and on-the-fly injecting initialization code that replaces nulls with defaults.

However, this is really difficult - a seasoned java programmer roughing will still probably require a full week to build such a tool. You'll need to look into java bytecode libraries such as ObjectWeb's ASM, or BCEL, because you need to read the bytecode first, and you'll have to generate bytecode, not java code.

Lombok

Lombok is often sold (sometimes even by us - I'm a core lombok contributor) as an 'annotation processor'. But it really isn't: It's a compiler plugin that simply loads via the Annotation Processor path sometimes. It does precisely what you are talking about and 'make a defaulting mechanism' is a very common request. You should probably search the ProjectLombok github home for issues about this and the wiki as there's plenty of words written on this idea already.

Given that lombok isn't an AP, if you want this, you'd fork lombok and then write it and use it, or, send in a PR if you find a really really neat way to do this. The primary reason we (lombok authors) aren't doing this yet is that annotation processors are extremely limited on what you can pass in for arguments, so we're not sure how to express the default value.

Just... code

It won't be automatic but you can of course always simply do something like this:

// code you actually write
static void myMethod(@NullToDefault String param) {
    param = applyDefaults("param", param);
    System.out.println(param);
}

i.e. you're going to have to manually write that applyDefaults line every time, but the code that leads to can at least read out that annotation. It won't be able to read out param (the name), hence, we pass it.

Upvotes: 1

Related Questions