Geek Time-The Beauty of Design Patterns Observer Pattern (Part 2) : 비동기 및 비 차단 EventBus 프레임 워크를 구현하는 방법은 무엇입니까?

관찰자 모드 : 동기식 차단은 주로 코드 분리를위한 가장 고전적인 구현 방법입니다. 비동기식 비 차단은 코드 분리를 달성 할뿐만 아니라 코드 실행 효율성도 향상시킬 수 있습니다. 프로세스 간 관찰자 모드 분리가 더 철저합니다. , 일반적으로 서로 다른 프로세스간에 관찰자와 관찰자 간의 상호 작용을 달성하는 데 사용되는 메시지 대기열을 기반으로 구현됩니다.

비동기식 비 차단 관찰자 패턴의 간단한 구현

비동기식 비 차단 관찰자 모드의 경우 실제로 다용 성과 재사용 성을 고려하지 않고 간단한 버전을 구현하는 것이 매우 쉽습니다.

두 가지 방법이 있습니다. 하나는 각 handleRegSuccess () 함수에서 코드 로직을 실행하는 새 스레드를 생성하는 것이고, 다른 하나는 UserController의 register () 함수에서 스레드 풀을 사용하여 각 관찰자의 handleRegSuccess () 함수를 실행하는 것입니다. 두 구현의 특정 코드는 다음과 같습니다.


// 第一种实现方式,其他类代码不变,就没有再重复罗列
public class RegPromotionObserver implements RegObserver {
    
    
  private PromotionService promotionService; // 依赖注入

  @Override
  public void handleRegSuccess(Long userId) {
    
    
    Thread thread = new Thread(new Runnable() {
    
    
      @Override
      public void run() {
    
    
        promotionService.issueNewUserExperienceCash(userId);
      }
    });
    thread.start();
  }
}

// 第二种实现方式,其他类代码不变,就没有再重复罗列
public class UserController {
    
    
  private UserService userService; // 依赖注入
  private List<RegObserver> regObservers = new ArrayList<>();
  private Executor executor;

  public UserController(Executor executor) {
    
    
    this.executor = executor;
  }

  public void setRegObservers(List<RegObserver> observers) {
    
    
    regObservers.addAll(observers);
  }

  public Long register(String telephone, String password) {
    
    
    //省略输入参数的校验代码
    //省略userService.register()异常的try-catch代码
    long userId = userService.register(telephone, password);

    for (RegObserver observer : regObservers) {
    
    
      executor.execute(new Runnable() {
    
    
        @Override
        public void run() {
    
    
          observer.handleRegSuccess(userId);
        }
      });
    }

    return userId;
  }
}

첫 번째 구현에서는 스레드를 자주 생성하고 삭제하는 데 시간이 많이 걸리고 동시 스레드 수를 제어 할 수 없습니다. 스레드를 너무 많이 생성하면 스택 오버플로가 발생합니다. 두 번째 구현에서는 스레드 풀을 사용하여 첫 번째 구현의 문제를 해결했지만 스레드 풀과 비동기 실행 논리가 모두 register () 함수에 결합되어 비즈니스 코드의이 부분에 대한 유지 관리 비용이 증가합니다.

요구 사항이 더 극단적이고 동기식 차단과 비동기식 비 차단간에 유연하게 전환해야하는 경우 UserController의 코드를 지속적으로 수정해야합니다. 또한 프로젝트에서 둘 이상의 비즈니스 모듈이 비동기식 비 차단 관찰자 모드를 사용해야하는 경우 이러한 코드 구현을 재사용 할 수 없습니다.

** 프레임 워크의 역할은 구현 세부 사항을 숨기고, 개발 난이도를 줄이고, 코드 재사용을 달성하고, 비즈니스 및 비 비즈니스 코드를 분리하고, 프로그래머가 비즈니스 개발에 집중할 수 있도록하는 것입니다. ** 비동기식 비 차단 관찰자 모드의 경우 프레임 워크로 추상화하여이 효과를 얻을 수도 있습니다.이 프레임 워크는이 강의에서 설명 할 EventBus입니다.

EventBus 프레임 워크 기능 요구 사항 소개

EventBus는 관찰자 모드를 구현하기위한 스켈레톤 코드를 제공하는 "이벤트 버스"로 변환됩니다. 이 프레임 워크를 기반으로 처음부터 개발할 필요없이 자체 비즈니스 시나리오에서 관찰자 모드를 쉽게 구현할 수 있습니다. 그중 Google Guava EventBus는 잘 알려진 EventBus 프레임 워크로 비동기식 비 차단 모드를 지원할뿐만 아니라 동기식 차단 모드도 지원합니다.

이제 Guava EventBus에 어떤 기능이 있는지 예를 들어 보겠습니다. 이전 강의의 사용자 등록 예제입니다. Guava EventBus로 다시 구현해 보겠습니다. 코드는 다음과 같습니다.


public class UserController {
    
    
  private UserService userService; // 依赖注入

  private EventBus eventBus;
  private static final int DEFAULT_EVENTBUS_THREAD_POOL_SIZE = 20;

  public UserController() {
    
    
    //eventBus = new EventBus(); // 同步阻塞模式
    eventBus = new AsyncEventBus(Executors.newFixedThreadPool(DEFAULT_EVENTBUS_THREAD_POOL_SIZE)); // 异步非阻塞模式
  }

  public void setRegObservers(List<Object> observers) {
    
    
    for (Object observer : observers) {
    
    
      eventBus.register(observer);
    }
  }

  public Long register(String telephone, String password) {
    
    
    //省略输入参数的校验代码
    //省略userService.register()异常的try-catch代码
    long userId = userService.register(telephone, password);

    eventBus.post(userId);

    return userId;
  }
}

public class RegPromotionObserver {
    
    
  private PromotionService promotionService; // 依赖注入

  @Subscribe
  public void handleRegSuccess(Long userId) {
    
    
    promotionService.issueNewUserExperienceCash(userId);
  }
}

public class RegNotificationObserver {
    
    
  private NotificationService notificationService;

  @Subscribe
  public void handleRegSuccess(Long userId) {
    
    
    notificationService.sendInboxMessage(userId, "...");
  }
}

EventBus 프레임 워크에 의해 구현 된 관찰자 패턴은 처음부터 작성된 관찰자 패턴과 비교됩니다. 대형 프로세스의 관점에서 보면 실현 아이디어는 거의 동일합니다. Observer를 정의하고 register () 함수를 통해 Observer를 등록해야합니다. 함수 (예 : EventBus의 post () 함수)를 호출하여 관찰자에게 메시지를 보냅니다 (메시지는 EventBus에서 이벤트라고 함).

그러나 구현 세부 사항 측면에서 다소 다릅니다. EventBus를 기반으로 Observer 인터페이스를 정의 할 필요가 없습니다. 모든 유형의 객체를 EventBus에 등록 할 수 있습니다. @Subscribe 주석은 클래스에서 관찰자가 보낸 메시지를받을 수있는 함수를 나타내는 데 사용됩니다.

다음으로 Guava EventBus의 몇 가지 주요 클래스와 기능에 대해 자세히 설명합니다.

● EventBus 、 AsyncEventBus


EventBus eventBus = new EventBus(); // 同步阻塞模式
EventBus eventBus = new AsyncEventBus(Executors.newFixedThreadPool(8))// 异步阻塞模式

● register () 함수

EventBus 클래스는 관찰자를 등록하는 register () 함수를 제공합니다. 구체적인 기능 정의는 아래와 같습니다. 모든 유형 (Object)의 관찰자를 수용 할 수 있습니다. 클래식 옵저버 패턴 구현에서 register () 함수는 동일한 옵저버 인터페이스를 구현하는 클래스 객체를 받아야합니다 .


public void register(Object object);

● unregister () 함수

register () 함수와 비교하여 unregister () 함수는 EventBus에서 관찰자를 삭제하는 데 사용됩니다. 많이 설명하지 않겠습니다. 구체적인 기능 정의는 다음과 같습니다.


public void unregister(Object object);

● post () 함수

EventBus 클래스는 관찰자에게 메시지를 보내는 post () 함수를 제공합니다. 특정 기능 정의는 다음과 같습니다.


public void post(Object event);

고전적인 관찰자 패턴과 다른 점은 post () 함수를 호출하여 메시지를 보낼 때 메시지가 모든 관찰자에게 전송되는 것이 아니라 일치하는 관찰자에게 전송된다는 것입니다. 소위 일치 가능이란 수신 할 수있는 메시지 유형이 전송 된 메시지 유형 (사후 함수 정의의 이벤트)의 상위임을 의미합니다. 예를 들어 설명하겠습니다.

예를 들어, AObserver가받을 수있는 메시지 유형은 XMsg, BObserver가받을 수있는 메시지 유형은 YMsg, CObserver가받을 수있는 메시지 유형은 ZMsg입니다. 그중 XMsg는 YMsg의 상위 클래스입니다. 다음과 같이 메시지를 보낼 때 메시지를받을 수있는 매칭 된 옵저버는 다음과 같습니다.


XMsg xMsg = new XMsg();
YMsg yMsg = new YMsg();
ZMsg zMsg = new ZMsg();
post(xMsg); => AObserver接收到消息
post(yMsg); => AObserver、BObserver接收到消息
post(zMsg); => CObserver接收到消息

각 옵저버가 수신 할 수있는 메시지 유형은 어디에 정의되어 있습니까? Guava EventBus의 가장 특별한 측면 중 하나 인 @Subscribe 주석을 살펴 보겠습니다 .

● @Subscribe 주석

EventBus는 @Subscribe 주석을 사용하여 함수가 수신 할 수있는 메시지 유형을 나타냅니다. 구체적인 사용 코드는 아래와 같습니다. DObserver 클래스에서 두 함수 f1 ()과 f2 ()를 @Subscribe로 주석 처리했습니다.


public DObserver {
    
    
  //...省略其他属性和方法...
  
  @Subscribe
  public void f1(PMsg event) {
    
     //... }
  
  @Subscribe
  public void f2(QMsg event) {
    
     //... }
}

DObserver 클래스 객체가 register () 함수를 통해 EventBus에 등록되면 EventBus는 @Subscribe 주석에 따라 f1 () 및 f2 ()를 찾아 두 함수가받을 수있는 메시지 유형 (PMsg-> f1, QMsg)을 기록합니다. -> f2). post () 함수를 통해 메시지 (QMsg 메시지 등)를 보내면 EventBus는 이전 레코드 (QMsg-> f2)를 통해 해당 함수 (f2)를 호출합니다.

수동으로 EventBus 프레임 워크 구현

이미 Guava EventBus의 기능을 명확하게 설명했으며 일반적으로 비교적 간단합니다. 다음으로 휠을 반복하고 EventBus를 "복사"합니다.

EventBus에서 두 가지 핵심 함수 register ()와 post ()의 실현 원리에 초점을 맞 춥니 다. 그것들을 이해하고 기본적으로 전체 EventBus 프레임 워크를 이해하십시오. 다음 두 그림은이 두 기능의 구현 회로도입니다.

여기에 사진 설명 삽입
여기에 사진 설명 삽입
그림에서 가장 중요한 데이터 구조는 메시지 유형과 수신 가능한 메시지 기능 간의 대응 관계를 기록하는 Observer 레지스트리라는 것을 알 수 있습니다. 관찰자를 등록하기 위해 register () 함수가 호출되면 EventBus는 @Subscribe 주석을 구문 분석하여 관찰자 레지스트리를 생성합니다. post () 함수를 호출하여 메시지를 보내면 EventBus는 레지스트리를 통해 메시지를받을 수있는 해당 함수를 찾아 동적으로 객체를 생성하고 Java의 리플렉션 구문을 통해 함수를 실행합니다.

동기 차단 모드의 경우 EventBus는 스레드에서 순서대로 해당 함수를 실행합니다. 비동기 비 차단 모드의 경우 EventBus는 스레드 풀을 통해 해당 기능을 실행합니다. 원칙을 이해하면 구현이 훨씬 간단 해집니다. 전체 소형 프레임 워크의 코드 구현에는 EventBus, AsyncEventBus, Subscribe, ObserverAction, ObserverRegistry의 5 개 클래스가 포함됩니다. 다음으로이 5 가지 카테고리를 차례로 살펴 보겠습니다.

1. 구독 구독

관찰자의 어떤 기능이 메시지를 수신 할 수 있는지 나타내는 주석입니다.


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Beta
public @interface Subscribe {
    
    }

2. ObserverAction

ObserverAction 클래스는 @Subscribe로 주석이 달린 메서드를 나타내는 데 사용됩니다. 여기서 target은 관찰자 클래스를 나타내고 메서드는 메서드를 나타냅니다. 주로 ObserverRegistry 옵저버 레지스트리에서 사용됩니다.


public class ObserverAction {
    
    
  private Object target;
  private Method method;

  public ObserverAction(Object target, Method method) {
    
    
    this.target = Preconditions.checkNotNull(target);
    this.method = method;
    this.method.setAccessible(true);
  }

  public void execute(Object event) {
    
     // event是method方法的参数
    try {
    
    
      method.invoke(target, event);
    } catch (InvocationTargetException | IllegalAccessException e) {
    
    
      e.printStackTrace();
    }
  }
}

3. ObserverRegistry

ObserverRegistry 클래스는 앞에서 언급 한 Observer 레지스트리로 가장 복잡한 클래스로 프레임 워크의 거의 모든 핵심 로직이이 클래스에 있습니다. 이 클래스는 Java 리플렉션 구문을 많이 사용하지만 전체 코드는 이해하기 어렵지 않습니다. 더 까다로운 곳 중 하나는 CopyOnWriteArraySet을 사용하는 것입니다.

CopyOnWriteArraySet는 이름에서 알 수 있듯이 데이터를 쓸 때 새 집합을 만들고 원래 데이터를 새 집합에 복제합니다. 새 집합에 데이터를 쓴 후 이전 집합을 새 집합으로 바꿉니다. 이러한 방식으로 데이터를 쓸 때 데이터 읽기 작업이 영향을받지 않도록하여 읽기 및 쓰기 동시성 문제를 해결할 수 있습니다. 또한 CopyOnWriteSet은 잠금을 통해 동시 쓰기 충돌을 방지합니다. CopyOnWriteSet 클래스의 소스 코드를 확인하여 특정 함수를 한 눈에 볼 수 있습니다.


public class ObserverRegistry {
    
    
  private ConcurrentMap<Class<?>, CopyOnWriteArraySet<ObserverAction>> registry = new ConcurrentHashMap<>();

  public void register(Object observer) {
    
    
    Map<Class<?>, Collection<ObserverAction>> observerActions = findAllObserverActions(observer);
    for (Map.Entry<Class<?>, Collection<ObserverAction>> entry : observerActions.entrySet()) {
    
    
      Class<?> eventType = entry.getKey();
      Collection<ObserverAction> eventActions = entry.getValue();
      CopyOnWriteArraySet<ObserverAction> registeredEventActions = registry.get(eventType);
      if (registeredEventActions == null) {
    
    
        registry.putIfAbsent(eventType, new CopyOnWriteArraySet<>());
        registeredEventActions = registry.get(eventType);
      }
      registeredEventActions.addAll(eventActions);
    }
  }

  public List<ObserverAction> getMatchedObserverActions(Object event) {
    
    
    List<ObserverAction> matchedObservers = new ArrayList<>();
    Class<?> postedEventType = event.getClass();
    for (Map.Entry<Class<?>, CopyOnWriteArraySet<ObserverAction>> entry : registry.entrySet()) {
    
    
      Class<?> eventType = entry.getKey();
      Collection<ObserverAction> eventActions = entry.getValue();
      if (postedEventType.isAssignableFrom(eventType)) {
    
    
        matchedObservers.addAll(eventActions);
      }
    }
    return matchedObservers;
  }

  private Map<Class<?>, Collection<ObserverAction>> findAllObserverActions(Object observer) {
    
    
    Map<Class<?>, Collection<ObserverAction>> observerActions = new HashMap<>();
    Class<?> clazz = observer.getClass();
    for (Method method : getAnnotatedMethods(clazz)) {
    
    
      Class<?>[] parameterTypes = method.getParameterTypes();
      Class<?> eventType = parameterTypes[0];
      if (!observerActions.containsKey(eventType)) {
    
    
        observerActions.put(eventType, new ArrayList<>());
      }
      observerActions.get(eventType).add(new ObserverAction(observer, method));
    }
    return observerActions;
  }

  private List<Method> getAnnotatedMethods(Class<?> clazz) {
    
    
    List<Method> annotatedMethods = new ArrayList<>();
    for (Method method : clazz.getDeclaredMethods()) {
    
    
      if (method.isAnnotationPresent(Subscribe.class)) {
    
    
        Class<?>[] parameterTypes = method.getParameterTypes();
        Preconditions.checkArgument(parameterTypes.length == 1,
                "Method %s has @Subscribe annotation but has %s parameters."
                        + "Subscriber methods must have exactly 1 parameter.",
                method, parameterTypes.length);
        annotatedMethods.add(method);
      }
    }
    return annotatedMethods;
  }
}

4. 이벤트 버스

EventBus는 동기화를 차단하는 관찰자 패턴을 구현합니다. 코드를 보면 몇 가지 질문이있을 수 있는데 이것은 분명히 스레드 풀 Executor를 사용합니다. 사실, MoreExecutors.directExecutor ()는 Google Guava에서 제공하는 도구 클래스로, 다중 스레드로 보이지만 실제로는 단일 스레드입니다. 그 주된 이유는 코드 로직을 AsyncEventBus와 통합하고 코드를 재사용하기 위해서입니다.


public class EventBus {
    
    
  private Executor executor;
  private ObserverRegistry registry = new ObserverRegistry();

  public EventBus() {
    
    
    this(MoreExecutors.directExecutor());
  }

  protected EventBus(Executor executor) {
    
    
    this.executor = executor;
  }

  public void register(Object object) {
    
    
    registry.register(object);
  }

  public void post(Object event) {
    
    
    List<ObserverAction> observerActions = registry.getMatchedObserverActions(event);
    for (ObserverAction observerAction : observerActions) {
    
    
      executor.execute(new Runnable() {
    
    
        @Override
        public void run() {
    
    
          observerAction.execute(event);
        }
      });
    }
  }
}

5. AsyncEventBus

EventBus를 사용하면 AsyncEventBus 구현이 매우 간단합니다. 비동기식 비 차단 관찰자 모드를 실현하려면 더 이상 MoreExecutors.directExecutor ()를 사용할 수 없지만 생성자의 호출자가 스레드 풀에 주입해야합니다.


public class AsyncEventBus extends EventBus {
    
    
  public AsyncEventBus(Executor executor) {
    
    
    super(executor);
  }
}

지금까지 200 줄 미만의 코드를 사용하여 여전히 유용한 EventBus를 구현했으며 기능적으로는 Google Guava EventBus와 거의 동일합니다. 그러나 Google Guava EventBus Google Guava EventBus 의 소스 코드 를 살펴보면 구현 세부 사항 측면에서 현재 구현과 비교하여 실제로 레지스트리에서 메시지 일치 기능에 대한 검색 최적화와 같은 많은 최적화를 수행했음을 알 수 있습니다. 연산. 시간이 있다면 소스 코드를 읽어 보시기 바랍니다.

추천

출처blog.csdn.net/zhujiangtaotaise/article/details/110480324