如何通过@Aspect 为字段设置值?

问题描述

我有一个调用方法的服务。同样在这个服务中有一个字段需要通过方面使用自定义注释分配一个

服务

@Service
public class TestServiceImpl implements TestService {
    
    @MyAnnotation(value = "someName")
    private Boolean isActive;

    @Override
    public String success(String name) {
        return (isActive) ? "Yes" : "No";
    }
}

注释

@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = ElementType.FIELD)
public @interface MyAnnotation{
    String value() default "";
}

根据注解中传入的值,数据库中应该有一条记录有 is_active 字段,这个值应该赋值给服务中的 isActive 字段

解决方法

如果您想坚持使用 Spring AOP 或更灵活的应用程序设计,我还建议您实现另一种应用程序设计。例如,为什么不注释 success(..) 方法并在 @Around 通知中拦截该方法,该通知根据注释值返回数据库配置值?

如果你绝对坚持注释字段,你必须同意从 Spring AOP 切换到 native AspectJ using load-time weaving (LTW),因为 AspectJ 可以通过 get()set() 切入点拦截字段读/写访问。但是如果你的字段是私有的,你需要一个特权方面并且不能使用注解风格的语法。

这是我的完整 MCVE,在 Spring 之外使用 AspectJ 向您展示它的基本实现方式:

带有配置键的标记注释:

package de.scrum_master.app;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(RUNTIME)
@Target(FIELD)
public @interface Marker {
  String value() default "";
}

驱动程序应用:

package de.scrum_master.app;

import java.io.PrintStream;

public class Application {
  @Marker int number;
  @Marker("settingA") private boolean isActive;
  @Marker("settingB") private boolean isNice;

  public String success(String name) {
    return isActive ? "Yes" : "No";
  }

  public String nice(String name) {
    return isNice ? "Yes" : "No";
  }

  public static void main(String[] args) {
    Application app = new Application();
    app.number = 11;
    PrintStream out = System.out;
    out.println(app.number);
    out.println(app.success("foo"));
    out.println(app.nice("bar"));
  }

}

方面:

请阅读评论以更好地了解细节。建议 #1-#4 更像是使用 get()set() 的一般操作教程,最后一个建议(滚动到最后)是您在这种特定情况下实际要求的内容。

package de.scrum_master.aspect;

import de.scrum_master.app.Application;
import de.scrum_master.app.Marker;

/**
 * Please note 'privileged'. This only works in native AspectJ syntax,see
 * https://www.eclipse.org/aspectj/doc/next/adk15notebook/ataspectj-aspects.html:
 * "Privileged aspects are not supported by the annotation style."
 */
public privileged aspect MyAspect {
  /**
   * During field write access,there is no generic way to get the old field value.
   * But we can get the new value via 'args' and manipulate it when proceeding. 
   */
  void around(int newValue) :
    set(@Marker * *) && args(newValue)
  {
    int newValueUpdated = 3 * newValue;
    System.out.println(thisJoinPoint + " AROUND: newV = " + newValue + " -> " + newValueUpdated);
    proceed(newValueUpdated);
  }

  /**
   * If we exactly know which field to target,we can of course access its old value directly by binding the
   * target instance to an advice parameter or by just directly accessing a static field.
   * But if the field is private,the aspect must be privileged.
   */
  void around(int newValue,Application application) :
    set(int Application.number) && args(newValue) && target(application)
  {
    int oldValue = application.number;
    int newValueUpdated = newValue + 7;
    System.out.println(thisJoinPoint + " AROUND: " + "oldV = " + oldValue + ",newV = " + newValue + " -> " + newValueUpdated);
    proceed(newValueUpdated,application);
  }

  /**
   * During field read access,it is easy to get the old field value by just proceeding.
   * A new value can be set by returning it from the advice.
   * 
   * '!within(MyAspect)' avoids triggering the advice when reading the field from within this aspect.
   */
  int around():
    get(@Marker int *) && !within(MyAspect)
  {
    int oldValue = proceed();
    int newValue = oldValue + 1000;
    System.out.println(thisJoinPoint + " AROUND:  " + oldValue + " -> " + newValue);
    return newValue;
  }

  /**
   * For just intercepting the newly set field value,'after() returning' can be used.
   * For this simple case we do not need an 'around()' advice.
   * A new value cannot be set here,of course,because that has already happened.
   * 
   * Please note how this advice also captures the access to System.out in the application.
   */
  after() returning(Object newValue) :
    get(* *) && !within(MyAspect)
  {
    System.out.println(thisJoinPoint + " AFTER RET:  " + newValue);
  }

  /**
   * After the more general part,this advice actually answers the question under
   * https://stackoverflow.com/q/66563533/1082681:
   * 
   * We look for annotated boolean fields (can be adjusted to other types easily),* get the annotation value from the value bound via '@annotation()' and
   * use the value to fetch the desired config value from the DB.
   */
  boolean around(Marker marker):
    get(@Marker boolean *) && !within(MyAspect) && @annotation(marker)
  {
    System.out.println(thisJoinPoint + " Fetching config value for " + marker + " from DB");
    return getConfigValueFromDB(marker.value());
  }

  /**
   * DB access mock-up
   */
  private boolean getConfigValueFromDB(String key) {
    switch (key) {
      case "settingA": return true;
      case "settingB": return false;
      default: return false;
    }
  }
}

控制台日志:

set(int de.scrum_master.app.Application.number) AROUND: newV = 11 -> 33
set(int de.scrum_master.app.Application.number) AROUND: oldV = 0,newV = 33 -> 40
get(PrintStream java.lang.System.out) AFTER RET:  java.io.PrintStream@279f2327
get(int de.scrum_master.app.Application.number) AROUND:  40 -> 1040
get(int de.scrum_master.app.Application.number) AFTER RET:  1040
1040
get(boolean de.scrum_master.app.Application.isActive) Fetching config value for @de.scrum_master.app.Marker(value=settingA) from DB
Yes
get(boolean de.scrum_master.app.Application.isNice) Fetching config value for @de.scrum_master.app.Marker(value=settingB) from DB
No

免责声明:我想向您展示 AspectJ 的强大功能。你可以这样做,但就像我之前说的,这并不意味着你应该这样做。通过注释访问器方法而不是字段,您可以轻松地重新设计您的应用程序以使其与 Spring AOP 一起使用。或者就像 Alexander Katsenelenbogen 所说的那样,有一些方法可以完全不用 AOP,尽管我认为 AOP 是一种合适的方法,因为从外部源读取配置值是一个交叉问题。 >