使用 AspectJ 和 JMX 進行訊息流追蹤

工程 | Ben Hale | 2006年4月25日 | ...

在我曾經工作的一個專案中,我們有一個系統負責接收來自裝置的消,並決定是否將該資訊傳遞給使用者。該系統有多個決策層級,我們遇到的問題之一始終是:訊息在透過系統的過程中是否丟失了?

在我們轉向 Spring 之前,幾乎不可能回答這個問題。我們嘗試使用日誌記錄,但由於做出決策的訊息數量龐大,這種方法充其量只能說是繁瑣。也嘗試使用偵錯程式,但龐大的資料量和時序變化導致成功率時有時無。

遺憾的是,我離開時我們還沒能實現一個更合適的解決方案,但如果當時做了,它可能會是這樣的。最後,我將討論在這種工作中可能有用的一些擴充套件。

首先,我們有一組介面及其實現


package flowtracingexample;

public interface Component1 {

	void forwardCall();

}

package flowtracingexample;

import java.util.Random;

public class DefaultComponent1 implements Component1 {
	
	private Component2 child;

	private Random r = new Random();
	
	public DefaultComponent1(Component2 child) {
		this.child = child;
	}

	public void forwardCall() {
		if (r.nextBoolean()) {
			child.forwardCall();
		}
	}

}

package flowtracingexample;

public interface Component2 {

	void forwardCall();

}

package flowtracingexample;

import java.util.Random;

public class DefaultComponent2 implements Component2 {
	
	private Component3 child;

	private Random r = new Random();
	
	public DefaultComponent2(Component3 child) {
		this.child = child;
	}

	public void forwardCall() {
		if (r.nextBoolean()) {
			child.forwardCall();
		}
	}

}

package flowtracingexample;

public interface Component3 {

	void forwardCall();

}

package flowtracingexample;

public class DefaultComponent3 implements Component3 {

	public void forwardCall() {
	}

}

這是一個非常簡單的例子,但要點是使用 fowardCall() 方法時,訊息有 50% 的時間會傳遞給下一個子元件(在本例中按數字升序)。請注意,這些 POJO 中沒有任何與追蹤相關的邏輯。

為了實現我們的追蹤行為,我們希望有一組計數器;每個元件一個。此外,我們希望能夠重置計數器、啟動和停止監控,並確定監控是否正在進行。為此,我們實現了一個包含這些計數器的類。


package flowtracingexample;

import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;

@ManagedResource
public class FlowTracer {

	private long component1Count = 0;

	private long component2Count = 0;

	private long component3Count = 0;

	private boolean tracing = false;

	@ManagedAttribute
	public long getComponent1Count() {
		return this.component1Count;
	}

	@ManagedAttribute
	public long getComponent2Count() {
		return this.component2Count;
	}

	@ManagedAttribute
	public long getComponent3Count() {
		return this.component3Count;
	}

	@ManagedAttribute
	public boolean getTracing() {
		return this.tracing;
	}

	public void incrementComponent1Count() {
		if (this.tracing) {
			component1Count++;
		}
	}

	public void incrementComponent2Count() {
		if (this.tracing) {
			component2Count++;
		}
	}

	public void incrementComponent3Count() {
		if (tracing) {
			component3Count++;
		}
	}

	@ManagedOperation
	public void resetAllComponentCount() {
		resetComponent1Count();
		resetComponent2Count();
		resetComponent3Count();
	}

	@ManagedOperation
	public void resetComponent1Count() {
		this.component1Count = 0;
	}

	@ManagedOperation
	public void resetComponent2Count() {
		this.component2Count = 0;
	}

	@ManagedOperation
	public void resetComponent3Count() {
		this.component3Count = 0;
	}

	@ManagedOperation
	public void startTracing() {
		tracing = true;
	}

	@ManagedOperation
	public void stopTracing() {
		tracing = false;
	}
}

這個類的方法及其內容都相當直觀。對您來說可能比較新的是這個類上的註解。當每個 bean 部署到 JMX MBeanServer 時,Spring 的 JMX 支援會使用這些註解自動構建 MBean 管理介面。

  • ManagedResource:宣告該類應作為 JMX MBean 暴露
  • ManagedAttribute:宣告此 getter/setter 表示的 JavaBean 屬性應為 MBean 屬性。如果需要對此屬性進行讀寫訪問,則需要同時註解 getter 和 setter。
  • ManagedOperation:宣告此方法應作為 MBean 操作暴露

最後,是將所有內容連線起來的問題。首先,我們將構成流程的元件連線起來。接下來,我們宣告將追蹤器應用於每個元件的切面。在本例中,我們使用了非常出色的 AspectJ 切入點語言。最後,我們設定 JMX exporter,使其能夠自動檢測帶有 @ManagedResource 註解的類例項。


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop 
       http://www.springframework.org/schema/aop/spring-aop.xsd">

	<!-- Components -->
	<bean id="component3" class="flowtracingexample.DefaultComponent3" />

	<bean id="component2"
		class="flowtracingexample.DefaultComponent2">
		<constructor-arg ref="component3" />
	</bean>

	<bean id="component1"
		class="flowtracingexample.DefaultComponent1">
		<constructor-arg ref="component2" />
	</bean>

	<!-- Aspect -->
	<bean id="flowTracer" class="flowtracingexample.FlowTracer" />

	<aop:config>
		<aop:aspect id="component1Aspect" ref="flowTracer">
			<aop:before method="incrementComponent1Count"
				pointcut="execution(public void flowtracingexample.Component1.forwardCall())" />
		</aop:aspect>

		<aop:aspect id="component2Aspect" ref="flowTracer">
			<aop:before method="incrementComponent2Count"
				pointcut="execution(public void flowtracingexample.Component2.forwardCall())" />
		</aop:aspect>

		<aop:aspect id="component3Aspect" ref="flowTracer">
			<aop:before method="incrementComponent3Count"
				pointcut="execution(public void flowtracingexample.Component3.forwardCall())" />
		</aop:aspect>
	</aop:config>

	<!-- JMX -->
	<bean class="org.springframework.jmx.export.MBeanExporter">
		<property name="autodetectModeName" value="AUTODETECT_ALL" />
		<property name="assembler">
			<bean
				class="org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler">
				<property name="attributeSource">
					<bean
						class="org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource" />
				</property>
			</bean>
		</property>
		<property name="namingStrategy">
			<bean
				class="org.springframework.jmx.export.naming.IdentityNamingStrategy" />
		</property>
	</bean>

</beans>

接下來我們需要做的是有一個驅動類。在本例中,驅動類只是以小於 750ms 的隨機延遲傳送訊息。


package flowtracingexample;

import java.io.IOException;
import java.util.Random;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class FlowTracingExample {

	public static void main(String[] args) throws InterruptedException,
			IOException {
		ApplicationContext ctx = new ClassPathXmlApplicationContext(
				"classpath:flowtracingexample/applicationContext.xml");

		Component1 comp = (Component1) ctx.getBean("component1");
		Random r = new Random();

		System.out.print("Ready...");
		 System.in.read();

		for (;;) {
			comp.forwardCall();
			Thread.sleep(r.nextInt(750));
		}
	}
}

在我的情況下,我將執行此應用程式並啟用 Java VM 管理功能,因為它為我提供了一個免費的 MBean 伺服器(而且我喜歡那些漂亮的記憶體圖)。如果您沒聽說過它,它是 Java 5 VM 中的一個系統屬性,可以讓 VM 使用 JMX 管理自身。它包含了記憶體消耗、執行緒等許多內容的 bean。您只需在執行應用程式的命令列中新增 -Dcom.sun.management.jmxremote 即可啟動它。作為 Java 5 的另一個巧妙補充,我將使用 jconsole 來顯示結果。

根據我生疏的數學技能,從長遠來看,我期望看到 Component 1 被呼叫 100%,Component 2 被呼叫 50%,Component 3 被呼叫 25% 的時間。讓我們看看

Tracing Screen Shot

很高興我的機率記對了。最好的部分是這仍然符合良好的設計原則。例如,所有元件都不知道任何關於追蹤的事情,因為那不是它們的職責。此外,這個子系統的所有追蹤需求都包含在一個類中,並且有一個實現,符合 AOP 的需求與實現 1:1 的目標。最後,有了關閉追蹤的功能,任何效能影響都或多或少地被抵消了。我知道,我知道增加一個整數並不昂貴,但如果你的追蹤做了昂貴的事情,這個功能就很有用,而且你無需擔心是否要將其投入生產;你可以簡單地停用監控,直到客戶來電尋求支援。

所以這些圖表確實很漂亮,如果你知道你預期的百分比,它們甚至可以告訴你一些資訊,但你還能做些什麼呢?比如最後 100 條透過的訊息及其決策?比如記錄訊息被丟棄的原因?比如丟棄決策與訊息未到達管道末端之間的關聯?如果你從未故意丟棄某條訊息,但它在進入 500ms 內未能到達末端(可能是由於執行緒問題),能夠知道該訊息已丟失,難道不好嗎?沿著同樣的思路,如果訊息從管道一端到達另一端的時間超過 250ms,給管理員傳送一封電子郵件怎麼樣?

追蹤/監控的可能性是無限的(並且是可插拔的!)。你會用它做什麼呢?

當然,還有原始碼

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

搶先一步

VMware 提供培訓和認證,助您加速發展。

瞭解更多

獲取支援

Tanzu Spring 透過一項簡單的訂閱,為 OpenJDK™、Spring 和 Apache Tomcat® 提供支援和二進位制檔案。

瞭解更多

即將到來的活動

檢視 Spring 社群所有即將到來的活動。

檢視全部