Java注解和处理器如何将一个方法标记为仅可调用一次?

5
我需要能够标记方法,以便在调用超过一次时抛出RuntimeException。我试图强制执行一些单赋值语义,并且我的类的参数数量太多了,无法放入一个构造函数中。我需要使这些类能够识别JAXB,因此对象需要是可变的,但我想强制执行单赋值语义。我相信可以使用Aspects来实现这一点,但我真的很想能够使用自己的注释处理器。我知道如何在Python中使用装饰器完成这个任务。如何编写一个注释处理器,可以在运行时拦截对注释方法的调用,而不仅仅是在编译时?我认为使用动态代理拦截方法调用是可行的,我只需要找出如何将其与我的注释处理器集成即可。

动态代理要求您使用接口,这太麻烦了,我现在使用CGLib MethodInterceptor,要拦截和装饰的内容要求少得多,但需要增加依赖。


4
注解本身没有任何行为。它只是元数据,旨在被某些工具或框架使用。您可以编写一个方面,查找注解的存在并执行您想要的操作,但单独的注解不会有任何效果。 - JB Nizet
5个回答

3
没有现成的解决方案。而AspectJ似乎是在更普遍的情况下使其正常工作的唯一方法。正如JB Nizet所指出的,注释应该有一个解析器来解析它。
然而,我建议使用更好、更简单的解决方案——构建器模式。它是什么样子:
- 你有一个可变的FooBuilder(它也可以是一个静态内部类),每个字段都有一个setter和getter。 - FooBuilder有一个build()方法,它返回Foo的一个实例。 - Foo有一个只接受FooBuilder的构造函数,在那里你分配每个字段。
这样做的好处是:
  • Foo is immutable, which is your end goal
  • It is easy to use. You only set the fields that you need. Something like:

    Foo foo = new Foo.FooBuilder().setBar(..).setBaz(..).build();
    
那么构建器就可以具备JAXB意识。例如:
FooBuilder builder = (FooBuilder) unmarshaller.unmarshal(stream);
Foo foo = builder.build();

JAXB对象需要可变性,而你的要求是不可变对象。因此,构建器非常方便来弥合这种差异。


@Jarrod Roberson,请看我刚刚添加的最后一段。 - Bozho
哇,我觉得那可能正是我某个项目所需要的! - G_H
1
@Bozho,我倾向于绕过构建器的东西,添加一个标志“configured”,并在mutators上assertConfigurable()。稍后,如果需要启用mutator(比如JMX),只需删除断言即可。构建器模式看起来至少是重复冗长的。 - bestsss

2
这个问题与问题应用CGLib代理从注释处理器有些相似。
如果你想在注释处理器中改变原始源代码的行为,请看看http://projectlombok.org/是如何实现的。我认为唯一的缺点是lombok依赖于com.sun.*类。
由于我自己需要这种东西,所以我想知道是否有更好的方法来实现这一点,仍然使用注释处理器。

1

0

你可以使用其他方式而不是使用注释。

assert count++ != 0;

你需要每个方法一个计数器。

在运行时检查一个方法是否被调用一次是一个令人讨厌的要求。 - Peter Lawrey
1
我支持这种断言的方法,总是让我想知道为什么人们期望从“框架”中获得太多,而存在一种更加简单的解决方案(并且不需要学习任何奇怪的东西)。 - bestsss
@Peter,我从来没有投票,如果没有人这样做,我想我可以授予50个赏金。 - bestsss
我认为断言在运行时是被禁用的。我曾经使用过一些并惊讶地发现它们会让错误的事情通过,直到有人提醒我默认情况下它们没有打开。你的方法可以只抛出一个简单的运行时异常,比如IllegalStateException。 - G_H
1
@G_H,断言由于常量折叠优化的好处而具有零运行时成本... - bestsss
显示剩余4条评论

0

我有类似的需求。简而言之,当您在Spring中注入组件时,像A依赖于B,B依赖于A这样的循环依赖情况是完全可以的,但您需要将这些组件作为字段或设置器进行注入。构造函数注入会导致堆栈溢出。因此,我不得不为这些组件引入一个init()方法,与构造函数不同,它可能会被错误地调用多次。不用说,像这样的样板代码:

private volatile boolean wasInit = false;
public void init() {
  if (wasInit) {
    throw new IllegalStateException("Method has already been called");
  }
  wasInit = true;
  logger.fine("ENTRY");
  ...
}

随处可见开始出现。由于这并不是应用程序的关键点,我决定引入一种优雅的线程安全的一行解决方案,注重简洁而非速度:

public class Guard {
  private static final Map<String, Object> callersByMethods = new ConcurrentHashMap<String, Object>();
  
  public static void requireCalledOnce(Object source) {
    StackTraceElement[] stackTrace = new Throwable().getStackTrace();
    String fullClassName = stackTrace[1].getClassName();
    String methodName = stackTrace[1].getMethodName();
    int lineNumber = stackTrace[1].getLineNumber();
    int hashCode = source.hashCode();
    // Builds a key using full class name, method name and line number
    String key = new StringBuilder().append(fullClassName).append(' ').append(methodName).append(' ').append(lineNumber).toString();
    System.out.println(key);

    if (callersByMethods.put(key, source) != null) {
      throw new IllegalStateException(String.format("%s@%d.%s() was called the second time.", fullClassName, hashCode, methodName));
    }
  }
}

现在,由于我更喜欢在 DI 框架内构建应用程序,因此将 Guard 声明为组件,然后注入它,并调用实例方法 requireCalledOnce 可能听起来很自然。但是由于其通用性,静态引用更有意义。现在我的代码看起来像这样:

private void init() {
  Guard.requireCalledOnce(this);
  ...
}

当同一个对象的init被第二次调用时,这里会出现异常:

Exception in thread "main" java.lang.IllegalStateException: my.package.MyComponent@4121506.init() was called the second time.
    at my.package.Guard.requireCalledOnce(Guard.java:20)
    at my.package.MyComponent.init(MyComponent.java:232)
    at my.package.MyComponent.launch(MyComponent.java:238)
    at my.package.MyComponent.main(MyComponent.java:48)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接